最近几个做 Agent 项目的朋友不约而同来找我聊同一个问题:框架换了好几个,模型也上了最新的,但智能体表现总是不稳定——有时候能按预期执行,有时候答非所问,工具调用乱成一团。
我看了一圈他们的代码,问题基本都不是出在模型能力上,而是提示词这块太随意了。System Prompt 直接写在代码字符串里,工具描述想到哪写到哪,上下文快塞爆了也不知道裁剪。说白了,就是没把提示词当工程来管,更别提 Agent 场景下的编排了。
这个选题其实也是我们系列写作的第七篇,上一篇聊的是多轮会话与上下文管理,这一篇聚焦两块:一是提示词模板怎么管理才不失控,二是 Agent 场景下提示词怎么编排才能真正发挥模型能力。这篇不会跟你扯太多玄学,全部是能直接落到代码里的实践方法,适合正在做智能体应用、已经在用 LangGraph / AutoGen / Coze 这类框架但觉得效果不满意的开发者参考。
1. 提示词模板管理:为什么值得做成一套工程体系
很多人的项目里提示词是这么存的:一个 Python 文件里十几个三引号字符串,有的带f-string,有的不带,模型换一个就全局搜索替换,加个需求得小心翼翼。这种搞法在前几个 Demo 里没什么问题,但一旦你的应用开始服务真实用户,模板数量上到几十上百个,必然出事。
提示词模板管理的本质是把“对话策略”和“业务逻辑”分开。你写代码负责的是流程、数据处理、工具调用,但模型怎么理解任务、按什么格式输出、边界在哪里,这些是策略。策略应该像配置文件一样独立维护,而不是散落在代码里。这样产品想调一句人设风格、法务想加一条合规约束,不需要开发写个需求单再改代码,直接改模板内容就行。
1.1 从“写一段prompt”到“管理一堆prompt”
我先给你看一个典型的反例。某团队做一个客服 Agent,最初只有一个 System Prompt,写在主程序里:
system_prompt = "你是XX公司的客服助手,请根据知识库回答问题。"后来加了订单查询、退款处理、投诉安抚,又加了多语言支持、敏感话题拒答、营销话术规范。三个月后,这个字符串变成了三千字,中间穿插着十几个if分支去拼不同的指令片段,任何一次改动都可能让另一条逻辑失效。
这其实不是提示词的问题,而是管理方式的问题。当你的提示词从一段演变成一套,就需要给它设计结构:拆分、命名、版本、渲染、引用。我在实践中比较推荐把提示词拆成三层,专门整理了一套模板分层模型,用表格表示就是这样:
| 层级 | 定位 | 典型内容 | 变动频率 |
|---|---|---|---|
| 基础层 | 通用能力与安全底线 | 角色定位、通用行为准则、敏感内容拒答策略 | 极低 |
| 业务层 | 具体业务逻辑与知识边界 | 业务流程说明、可选操作清单、特定场景话术 | 中 |
| 会话层 | 单次会话相关上下文 | 用户输入摘要、临时数据、当前步骤提示 | 高 |
分层的逻辑很直白:基础层类似公司章程,改一次要高层审批;业务层类似部门制度,随业务调整而变;会话层类似每日待办清单,每次会话都要刷新。写代码时你肯定会分层,提示词为什么不分层?不分层的问题在于,当某个模板变成三千字之后,你根本说不清楚是哪个模块导致的模型行为异常,排查成本远超管理成本。
1.2 模板分层与元数据设计
落到具体实现上,模板不只存文本,还要存元数据。我设计的模板记录一般长这样:
{ "template_id": "order_refund", "name": "订单退款处理", "version": "2.3.1", "layer": "business", "model_compat": ["gpt-4o", "claude-sonnet", "qwen-max"], "variables": ["user_name", "order_id", "refund_reason"], "tags": ["电商", "客服", "售后"], "content": "...模板正文..." }这段元数据里,layer帮你定位改动影响面,version保证模型行为可回退,model_compat是踩坑换出来的经验——同一个模板在 GPT-4o 上表现很好,换到另一个模型上可能因为格式要求不同而崩溃。variables字段则把模板依赖的外部数据显式声明,渲染前就能做完整性校验,而不是等模板输出乱码了才发现漏传变量。
这类模板记录存成 JSON 文件或数据库表都可以,只要保证读取逻辑统一。元数据不需要一开始就很复杂,但template_id、version、content三件套必须有——没有版本号的模板管理就是耍流氓,模型行为变了你连对比样本都拉不出来。
1.3 变量、渲染与版本控制的落地细节
模板渲染听起来很简单,就是把变量插进去。实际做的时候坑不少。我在项目里用 Jinja2 做模板引擎,主要看中三个能力:变量缺失不静默、过滤器链、以及微调控制结构。
from jinja2 import Environment, StrictUndefined, meta from jinja2 import FileSystemLoader env = Environment( loader=FileSystemLoader("prompt_templates/"), undefined=StrictUndefined, # 变量缺失直接抛异常,不静默输出空串 autoescape=False, # 提示词里不要自动转义 HTML ) # 渲染前先检查模板引用的变量 def render_template(template_id: str, variables: dict) -> str: source = env.loader.get_source(env, f"{template_id}.j2")[0] parsed = env.parse(source) required_vars = meta.find_undeclared_variables(parsed) missing = required_vars - set(variables.keys()) if missing: raise ValueError(f"模板 {template_id} 缺少变量: {missing}") tmpl = env.get_template(f"{template_id}.j2") return tmpl.render(**variables)StrictUndefined是血泪教训换来的。默认行为下,Jinja2 遇到缺失变量会渲染成空字符串,你的提示词可能变成“用户,你好,你的订单已”,而代码完全不会报错。开启严格模式后,这类错误在开发期就暴露。渲染前用meta.find_undeclared_variables做变量完整性校验,相当于给提示词加了 TypeScript 的类型检查,值得养成习惯。
版本控制上我建议模板文件和代码走同一个 Git 仓库。这样每次改动模板都留下提交记录,配合元数据里的version字段,能随时用git diff看两个版本的行为差异。模板单独建目录,不跟业务代码混在一起,目录结构大致保持这样:
prompt_templates/ ├── base/ # 基础层 │ └── assistant_persona.j2 ├── business/ # 业务层 │ ├── order_refund.j2 │ ├── complaint_handling.j2 │ └── product_consult.j2 ├── session/ # 会话层 │ └── conversation_context.j2 └── test/ └── cases/ # 模板测试用例版本号建议用语义化版本:主版本表示破坏性更新(比如重写了整个角色设定),次版本表示新增能力(比如加了一条新的处理分支),修订号表示小改动(比如调整了一句措辞)。这套规则不复杂,但能让你出差错时快速定位是哪次改动引入了问题。
2. Agent提示词编排:不是拼积木,是分层设计
模板管理解决的是“存量怎么组织”的问题,Agent 提示词编排解决的是“增量怎么生成”的问题。两者关系是:模板是零件,编排是装配。很多人都听过 Agent = 大模型 + 规划 + 记忆 + 工具 这个公式,但落到提示词层面,怎么把这些模块按合理的次序与结构放进一次请求里,是有一番讲究的。
Agent 提示词编排和传统 Prompt Engineering 最大的区别在于:传统 Prompt 大多是单轮一次性对话,指向明确;Agent 场景下模型需要理解自己“能做什么”“不能做什么”“按什么顺序做什么”“遇到异常怎么办”,这就不是一段话能解决的了,必须编排成一套相互配合的指令体系。
2.1 Agent提示词的典型组成结构
我每次设计 Agent 的 System Prompt,脑子里都会过一个清单,你可以把这个清单当作编排框架。标准结构大致如下:
- 角色与目标:Agent 是谁,本轮服务的核心目标是什么。比如“你是电商售后助理,目标是尽快解决用户的售后问题,同时控制退款率在合理范围”。
- 行为准则:面对用户、面对信息缺失、面对冲突时的处理原则。这部分是模型的“价值观底座”,尤其要写清楚边界。比如“不得承诺超出政策范围的补偿”“不确定时先查规则再回答”。
- 可用工具与触发条件:列出 Agent 可以调用的工具,每个工具在什么场景下触发,不要一把梭全列出来,要有选择逻辑。
- 工作流程:由步骤组成的执行链路,比如先识别用户意图,再查询订单,再给出方案。复杂任务可以分组描述,让模型按阶段推进。
- 输出格式:什么场景用自然语言回复,什么场景输出结构化数据,什么场景必须调用工具。格式要求写得越具体,模型越不容易自由发挥。
- 限制与兜底:哪些话题不处理、连续失败怎么办、超时怎么办等异常分支的处理策略。
这六个部分是松耦合的,别把它们写成一整段连续文字。我在实际编排时,会刻意在段落之间留出清晰的区域标识(比如用标记性的段落标题),模型对这种结构化的提示词理解更稳,并能减少互相干扰。这是一个很实用的经验。
2.2 工具描述与Few-shot示例的规范写法
Agent 能不能正确调用工具,一半取决于工具本身的实现,另一半取决于工具描述写得怎么样。很多开发者的工具描述只有一句话“查询订单信息”,模型并不知道这个工具接收什么参数、什么时候该用、返回什么结果,于是经常出现误调用。
我在项目里把工具描述规范为四个部分:工具目标、输入参数说明、触发场景、输出说明。有一个专门整理的工具描述与功能配置的对照关系:
| 描述维度 | 常见错误写法 | 建议写法 |
|---|---|---|
| 工具目标 | 查询订单 | 根据订单ID查询一笔订单的最新状态,包含物流进度、退款状态、商品明细 |
| 输入参数 | 传入订单编号 | 参数 order_id:字符串,必填,例如 OD20250101001 |
| 触发场景 | 用户問订单情况时调用 | 用户询问订单进度、物流位置、发货时间、退款到账情况时调用 |
| 输出说明 | 返回订单信息 | 返回 JSON:status(状态)、logistics(物流轨迹数组)、refund(退款金额/状态) |
工具描述写不到位,模型就会“试探性调用”,先猜一个参数格式,失败了再猜一次,反复出错。所以写工具描述时要站在模型的角度问自己一个问题:如果我只看到这段描述,我知道什么时候该用这个工具、参数怎么填吗?如果答案是模糊的,模型大概率也是模糊的。
Few-shot 示例的写法同样重要,但要注意精确度。示例不是写得越多越好,通常每个工具 2-3 个典型调用示例足够了,关键是覆盖易混淆场景。我做客服 Agent 时发现,模型经常把“修改订单地址”和“取消订单”搞混,于是给两个工具各加了一个对比例子:
用户说“我想改收货地址”,不能调用 cancel_order, 而要调用 update_order_address,并保留原订单号信息。这种对比式示例比单纯堆功能示例有效得多。另外示例中的用户表述、工具调用、模型回复要完整配对,不要只给工具调用片段,那样模型学不到完整的决策链路。
2.3 上下文窗口管理与记忆注入策略
Agent 编排里最容易忽视的是上下文长度控制。你现在有模板、有示例、有工具描述、有历史对话摘要,一股脑塞进去,算一下 token 很可观。一旦执行 Agent 陷入长对话,超出上下文限制,模型要么忘记前面的指令,要么直接报错。
我的策略是把提示词分优先级,用“三梯队”方式管理上下文空间。第一梯队是核心指令区,系统提示词里最关键的段落(角色、行为准则、输出格式)保持在上下文前半部分,模型对这部分关注度最高。第二梯队是工具与示例区,只在执行到相关分支时才注入,避免无关工具占空间。第三梯队是历史与临时数据区,对历史消息做衰减式保留,最近的保留原文,较早的只保留摘要,再早的直接丢弃。
记忆注入也要讲策略,不要把整段历史塞进 Prompt。我会对历史消息做摘要,然后按“当前目标相关度”筛选注入。简单做法是维护一个滑动窗口:最近 5 轮全量保留,更早的每 3 轮压缩成一句摘要。这样 Agent 既保留长期上下文,又不至于让上下文爆掉。这与长时记忆系统的思路一致,在 Agent 架构里记忆不是照单全收,而是有选择地调度。
3. 实操:一套可落地的模板管理与Agent编排方案
理论铺垫完了,接下来是完整的可落地实践。我会带你走一遍我最近做的一个“客服售后 Agent”的真实流程,重点演示三部分:模板工程基础实现、Agent 编排流程实现、测试评估方法。你不需要照抄我的业务,直接把这个套路翻译到你的场景里即可。
3.1 模板工程的基础实现
第一步先把模板目录建好,我在项目里用 Jinja2 作为引擎,模板文件后缀统一用.j2做标识。先建一个基础人格模板:
{% raw %} 你是{{ company_name }}的智能售后助理,你的名字叫{{ assistant_name }}。 ## 行为准则 - 以解决用户问题为首要目标,态度友好但不卑不亢。 - 所有回复必须基于给定的知识库与订单数据,不得编造信息。 - 涉及退款、补偿等敏感操作,必须先列明依据政策再向用户说明。 - 如果用户情绪激动,先安抚情绪,再解决问题,不要急于反驳。 - 不回答与售后无关的问题,礼貌引导用户回到售后主题。 ## 输出要求 - 回复使用简体中文,语气自然,篇幅控制在 200 字以内。 - 如果信息不足,明确告知用户需要补充什么信息。 - 如果必须调用工具,先给出简短说明,再等待工具结果,不要凭空猜测。 {% endraw %}基础层的内容不涉及具体业务,是 Agent 的“人格与底线”。在写业务层模板时,我只关注具体售后类型的分支说明。比如售后分类模板里定义业务链路:
{% raw %} ## 售后分类处理 根据用户的描述,将售后请求分类,并按对应策略处理: 1. 订单查询:调用订单查询工具,获取 {{ user_name }} 名下最新订单信息。 2. 退货退款:确认订单状态与商品是否支持七天无理由退货。 3. 物流异常:查询物流轨迹,如有异常联系仓库处理。 4. 发票问题:引导用户登录后台查看电子发票。 无论哪种分类,最后都要给用户一个明确结论或下一步操作指引,不能只说“您的反馈已记录”。 {% endraw %}模板渲染与校验封装到统一模块里,这部分代码负责加载模板、检查变量、渲染、记录版本。后续 Agent 每次调用前,先走一遍渲染流程,保证拿到的 System Prompt 是完整且符合当前业务配置的。
3.2 Agent编排流程的实现
接下来把模板编排成真正的 Agent 请求。这里我用一个极简的编排函数演示,不依赖任何框架,方便你看清本质:
def build_agent_prompt(user_input: str, session_state: dict) -> list: # 1. 渲染基础层 base_prompt = render_template( "base/assistant_persona", {"company_name": "示例科技", "assistant_name": "小A"} ) # 2. 渲染业务层 intent = classify_intent(user_input) # 示例:简单分类函数 business_prompt = render_template( f"business/{intent}", {"user_name": session_state.get("user_name", "用户")} ) # 3. 组装工具描述(只注入当前意图相关的工具) tools = get_relevant_tools(intent) tool_prompt = format_tool_descriptions(tools) # 4. 注入会话层(历史摘要 + 当前用户输入) session_prompt = build_session_context(session_state) # 5. 按固定顺序拼接 system_prompt = "\n\n".join([ base_prompt, business_prompt, tool_prompt, "## 会话上下文", session_prompt ]) return [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input} ]这套编排有几个关键选择,解释一下我的考虑。省掉框架依赖是为了减少干扰,让你集中于 Prompt 组装逻辑,换任何框架(LangGraph、AutoGen、字节的 Coze 或自研框架)时这套逻辑都能平移。按意图注入相关工具,而不是把所有工具全塞进去,能明显降低模型工具选择错误的概率。拼接顺序固定,把基础层和业务层放在前面,会话上下文放最后,对模型注意力分配更友好。你可能会想为什么 System Prompt 里要带会话上下文,因为这样在多轮时模型不必每次都靠前面的对话记录推断当前状态,信息直接注入能减少遗忘。
3.3 测试与评估:如何判断编排出的提示词是好是坏
提示词编排完,怎么知道好还是不好?只跑几个 happy path 远远不够,我总结一套可执行的测试清单:
- 单轮正确率:20-50 条测试用例,覆盖典型用户问题,看回复是否符合预期格式与答案。
- 工具选择准确率:专门构造“易混淆场景”,看模型是否选对工具,参数是否正确,错误率高就回去调工具描述。
- 多轮稳定性:连续 10 轮以上对话,看模型是否保持人格一致、不跑偏、不忘记最初约束。
- 安全边界测试:输入诱导性、越权性问题,看模型是否坚守边界,会不会被带偏。
- 成本基线:记录每次调用的 token 数,排编前后对比,防止为了效果无节制塞长文本。
测试时可以把输入输出存成日志,标注通过/失败原因,这样每次调整模板后都能对比回归。我习惯维护一个测试用例集文件的独立仓库,与模板库并排管理,每次模板变更必须跑一遍全部用例,确保没有破坏既有能力。这一步花不了多少时间,但能让你在项目后期省下大把的“灵异 Bug 排查时间”。
4. 常见问题与排查技巧实录
所有方案在真实项目里都会踩坑,这里把我遇到过的典型问题整理成一份排查速查表,并附上定位思路与修复建议。这些都是平时文档里不太会写的,但大概率是你迟早遇上的。
4.1 变量冲突与花括号转义
Jinja2 用{{ }}做变量插值,但提示词里有时需要让模型输出 JSON 示例、花括号结构,结果模板一渲染,花括号被引擎吃掉,或者报错。我第一次写模板时,因为提示词里带 JSON 示例导致渲染全乱,排查了很久才发现是花括号冲突。
解决办法是给模板中需要原样输出的花括号部分用{% raw %}包起来,或者使用{{ '{{' }}转义。更省心的做法是预先约定模板中不直接写大段 JSON,改用占位符描述,例如写“输出 JSON 格式,包含 status 与 message 字段”,把具体格式在代码侧控制。日常建议对模板做自动化渲染测试,把测试用例挂在 CI 上,改模板后自动跑一遍所有渲染样例,花括号冲突这类问题在开发期就能被杀掉。
4.2 模型不兼容与模板漂移
同一套模板在 A 模型上效果好,换到 B 模型上效果差,这我在实际项目中经历过多次。原因各有不同:有的是因为模型对 Markdown 标题的敏感度不同,有的是偏好不同的指令措辞,有的是工具描述格式的接受度差异。元数据里的model_compat字段值得认真维护,每次验证通过后顺手记录一下。
“模板漂移”指的是模板内容在频繁修改中慢慢偏离最初设计,失去一致性。避免方案是版本控制加定期审查。我每个季度会做一次模板大扫除:把线上所有模板拉出来,检查是否有死代码、重复段落、过时表述,做一次合并整理。这就像代码重构,短期看不到收益,长期能保住可维护性。
4.3 编排后的提示词过长或截断
把模板、工具描述、Few-shot、历史摘要全拼起来,很容易达到上下文上限。尤其是工具数量多、每个工具描述写很长的时候。我处理过的极端案例里,一次请求的 System Prompt 超过了 12000 token,模型开始把注意力分散到不重要的部分,核心能力反而下降。
解决思路是给每个模块设预算上限。我给一个参考配置表:
| 模块 | 预算比例 | 说明 |
|---|---|---|
| 基础层角色与准则 | 20%-25% | 强约束,必须完整 |
| 业务层逻辑 | 20%-30% | 按当前任务动态裁剪 |
| 工具描述 | 20%-25% | 只注入相关工具 |
| Few-shot 示例 | 15%-20% | 按需精简 |
| 会话上下文 | 10%-15% | 摘要化呈现 |
这个比例不是死的,但能提供一个总量控制思路。配置工具时,一个工具的描述控制在 150-300 token 之间比较合适,超过这个范围就要考虑是不是把逻辑写进代码而不是提示词。预算意识很重要,Prompt 编排不是“越多越全越好”,是“每个 token 都有目的”。
4.4 工具描述失效与Agent循环失控
最常见的反馈是:模型明明有工具,却不用,或者调用了工具但结果不被正确采纳。排查时先看工具描述里是否写清楚了触发条件和输出说明,很多时候模型不调用不是因为不会,而是描述里没提到“什么情况下调用”。
有一种比较隐蔽的情况:模型在一个循环里反复调用同一工具,拿不到结果也不换策略,表现为“Agent 卡在某个状态”。解决方案是要在编排中注入尝试次数限制,例如明确写“同一工具调用失败两次后,停止调用并直接告知用户无法处理,建议转人工”。这属于典型的 Agent 安全边界,必须写进 System Prompt 的异常处理段落。我还习惯把工具调用的完整轨迹写进日志,出现循环时可以直接回溯每一步,定位是哪一步的决策分支没有设计好。Agent 的编排中控逻辑要兜底,不要把所有控制权都交给模型。
我在实际项目中的几条干货建议
最后说几句经验之谈,不算总结,就是这几年来反复验证过的几个观点。
第一句话:提示词模板管理永远值得从一开始就做。项目再小,只要你的 Agent 要长期维护,模板拆分和版本控制的前期成本都不高,但后期收益极大。不要等提示词膨胀到没法收拾才开始重构。
第二句话:Agent 提示词编排要把自己当“导演”而不是“编剧”。你的核心工作不是替模型写好每一句话,而是规划好阶段性目标、工具边界、异常处理路径,让模型在框架内有足够的发挥空间。一个编排良好的 Agent,不是提示词写得花哨,而是边界清晰、路径完整、兜底明确。
第三句话:所有技巧都要靠测试数据检验,不要凭感觉优化。我们很容易陷入“调整措辞玄学”的循环,一会儿觉得这句加得好,一会儿又改回去。坚持维护测试集、对比版本差异,才是可持续的优化方法,也符合我们系统化处理智能体问题的整体思路。
这套方法在实际项目里已经帮我扛过了好几次发版,模板管理和编排逻辑分离之后,产品和开发之间的协作也顺了很多。你可以先从最小的场景开始,把一个单机 Demo 的提示词按模板重写一遍,感受一下差异,下一步再看怎么引入更复杂的编排框架。