提示词写多了之后,你会发现单条 prompt 写得再漂亮,一旦涉及多个场景、多个智能体协同,立刻就会失控。我自己的项目从十几个零散提示词膨胀到上百个之后,第一次真切感受到什么叫“提示词也需要管理”。这篇是系列第七篇,我打算把提示词模板管理、Agent 提示词编排这两块完整梳理一遍:不只是给你一个模板文件或一段示例代码,而是讲清楚为什么这样做、踩过哪些坑、哪些参数值得反复调。适合已经写过不少 prompt、但还没把提示词工程化的朋友,也适合准备用 Dify、Coze 或自研框架做 Agent 开发的人。
1. 内容整体设计与思路拆解
提示词模板管理和 Agent 提示词编排,听起来是两个话题,但实际是同一件事的两面:把“模型输入”这件事从临场发挥变成可维护、可复用、可验证的工程产物。我拆解这个问题的核心思路是“先结构化,再动态化,最后流程化”。
1.1 为什么提示词需要模板管理:从零散到工程化
早期大家写 prompt 都是对话框里直接打,靠感觉调。一次两次没问题,一旦进入真实项目,问题立刻冒出来:不同接口、不同模型、不同场景之间,提示词风格完全不统一;同一个角色的 prompt 改了需求,所有相关的地方都要手动替换;想测试同一个提示词在不同模型上的效果,根本没法批量跑。这就像做菜全靠厨师临场发挥,菜谱写在纸条上,今天这个厨师做了,明天换人味道就变了。
模板管理的本质,是把 prompt 变成“静态骨架 + 动态变量 + 条件片段”的组合。静态骨架是角色设定、任务流程、输出格式这些不变的部分;动态变量是用户输入、上下文、工具返回结果;条件片段则是根据场景决定是否插入的示例或约束。这样做的好处有三个:第一,复用,一个客服模板可以服务所有客服类任务;第二,可控,修改只会落在变量和条件片段上;第三,可测试,同样的模板可以批量跑不同变量的组合,方便比较效果。
在我自己的实践里,模板管理最大的价值还不只是省事,而是让 prompt 可以像代码一样被 review。团队里有人改了系统提示词,其他人能通过 diff 看到改了什么,为什么改。这个能力在 Agent 项目里尤其重要,因为提示词一旦出错,很难靠肉眼在几百行上下文里定位问题。
1.2 Agent 编排的核心逻辑:从单轮到多智能体协同
单独一个 Agent 可以理解成“模型 + 工具 + 记忆 + 循环决策”。但真实任务很少是一个 Agent 从头跑到尾的,比如写一篇行业分析报告,可能需要一个规划 Agent 拆分任务,一个检索 Agent 找资料,一个写作 Agent 组织内容,一个审校 Agent 检查事实错误。这时候真正困难的就是编排:怎么定义每个 Agent 的职责边界,怎么传递中间结果,怎么决定下一步走哪个分支。
编排的核心逻辑不是“让多个 Agent 聊个天”,而是把任务拆解成有依赖关系的子步骤,每个子步骤由一个或多个 Agent 承担,并在它们之间传递可控的上下文。我常用的编排模式有四种:单 Agent 循环,也就是经典的 ReAct 模式,模型自己决定调什么工具、观察结果、继续推理;规划-执行模式,先用一个 Agent 生成计划,再按计划逐个执行;多 Agent 协作模式,每个 Agent 有独立角色,通过消息池交互;工作流编排模式,像 Dify、Coze 那样用可视化节点把 Agent 和工具串起来。
选哪种模式不是越复杂越好。我见过很多团队一上来就搞六个 Agent,结果互相干扰、上下文混乱,效果还不如单 Agent 加几个好工具。我的建议是:能用单 Agent 解决的,不要硬上多 Agent;多 Agent 的收益主要体现在任务步骤之间有明显依赖、需要不同模型能力协同、或者单个 Agent 上下文撑不住的时候。
1.3 方案选型:自研模板库 vs 平台编排
做提示词模板管理和 Agent 编排,通常会面临两条路线:一条是直接用 Dify、Coze 这样的平台,另一条是自己写代码管理模板并用框架编排 Agent。我自己两条路线都走过,选型的关键是看团队技术栈和迭代速度。
平台方案的优势是可视化、上手快。Dify 的工作流编排里,你可以把提示词模板挂在节点上,用变量把前一个节点的输出传给下一个节点,调试的时候还能看每步的输入输出。Coze 也是类似思路,适合快速验证业务逻辑。但平台也有隐忧:模板更新和版本管理不够灵活,复杂的条件分支写起来比较受限于平台节点类型,而且如果业务需要深度集成自己的模型评估、日志审计体系,平台的开放程度往往不够。
自研方案的核心是用代码管理模板,再加上 Agent 运行时框架。比如用 Jinja2 做模板渲染,用 Python 写一个简单的 Agent 循环,用 LangGraph 之类的框架处理图结构编排。这样做的好处是灵活,模板即代码,测试、版本管理、灰度发布都能跟现有工程体系衔接;坏处是要自己处理很多细节,比如变量校验、上下文裁剪、重试策略。我的建议是:小团队或业务探索期,先上平台试原型;等模型逻辑稳定了,再把核心流程迁到自研跑生产。
2. 核心细节解析与实操要点
把方案定下来之后,真正决定效果的是细节。模板管理不是建几个文件就完事,而是要把每个字段都设计得经得起折腾。这一节我重点讲模板结构、分层设计、版本管理。
2.1 模板管理的关键元素:变量、指令块、示例、约束
一个合格的提示词模板,至少应该包含以下部分:模板元信息、系统指令、用户指令、示例、变量声明、约束条件。
| 元素 | 作用 | 示例 |
|---|---|---|
| 模板元信息 | 记录名称、版本、适用场景、模型 | name: customer_service, version: 1.2.0 |
| 系统指令 | 定义角色、任务目标、基本规则 | 你是一名电商客服,只回答订单相关的问题 |
| 用户指令 | 接收本次任务的具体输入 | 用户问题:{{ question }} |
| 示例 | 给模型少样本示范 | 输入:xxx,输出:xxx |
| 变量声明 | 标注哪些是可替换字段、默认值、必填性 | question: string, required |
| 约束条件 | 限制输出格式、长度、风格 | 必须使用 Markdown 列表输出 |
我踩过最大的坑是“变量命名混乱”。早期模板里既有{user_input}又有{{ user_input }},还有query、question混着用,渲染的时候经常漏传。后来我定了三条规矩:所有模板统一用双花括号{{ variable }};所有变量名用蛇形命名,集中在模板头部的variables字段声明;变量必须区分必填和可选,可选变量给默认值。这样改完之后,变量传错的问题减少了八成。
指令块的设计也值得多说一句。不要把任务分解、处理步骤、输出要求全糊在一大段里。我会把固定不变的角色和规则放在 system 指令,把一次性任务信息放进 user 指令,把可复用示例放在 fewshot 区域。这样将来如果要切换模型或者调整其中一部分,不需要整个模板重写。
2.2 Agent 提示词的分层设计:系统提示、任务提示、步骤提示
Agent 的提示词和普通 prompt 不一样,它还承担着“调度”功能。如果把所有内容塞进一个 System Prompt,Agent 在长对话里很容易遗忘后端的细节。我的做法是分层设计。
第一层是全局系统提示,它是 Agent 的根配置文件,包括身份、核心原则、可调用的工具列表、输出偏好。这部分稳定,不随任务变化。第二层是任务提示,表示当前这一步具体要做什么,由外部输入触发,比如用户问题、前端传来的结构化任务。第三层是步骤提示,出现在 Agent 循环内部,比如“你刚完成了搜索,现在请基于搜索结果回答”这个指令,就是典型步骤提示。
用一个实际例子说明:我做过一个技术问答 Agent,它的全局系统提示里写了角色是“资深后端工程师”,规则包括“不知道的不要编造,直接说不确定”,工具列表包含代码搜索引擎和文档库。任务提示则是由用户问题渲染出来的{{ question }}。当 Agent 第一次调用完搜索工具后,步骤提示会自动追加一句“请基于工具返回的结果,给出代码示例并说明优缺点”。这三层各司其职,改起来也互不影响。
这里要特别提醒:步骤提示不要写成“根据以上内容回答”,太模糊。模型可不会自动知道什么是“以上内容”,你需要明确告诉它参考哪些字段、忽略哪些字段。比如“忽略工具返回结果的第 3 条,只使用前 2 条”,这种指令虽然看起来笨,但效果非常稳定。
2.3 模板版本管理与多环境适配
提示词模板不是写一版就完了。模型升级、业务需求变化、评测发现偏差,都会让模板需要迭代。我强烈建议把模板纳入 Git 管理,每个改动都有提交记录,并且用版本号标记。版本号格式我习惯用语义化版本:主版本号变更代表角色或任务流程有破坏性调整,次版本号代表新增变量或约束,补丁号代表措辞修改。
多环境适配也是容易被忽略的点。开发环境、测试环境、生产环境可能需要不同的 API Key、不同模型版本、甚至不同的提示词措辞。我的解决方案是模板里的环境相关的内容也用变量表示,比如{{ model_name }}、{{ temperature }},在渲染时根据环境注入。这样就能保证同一份模板在 dev 和 prod 之间切换时,除了环境变量之外没有任何差异。
再提一个 A/B 测试的技巧:不要在生产环境直接换模板,先复制一个带实验标记的模板,比如customer_service_v1_3_ab_test,然后把流量切一部分到新模板,观察输出质量和人工评价指标。等数据出来之后,再决定是否把实验模板提升为正式版本。这个过程跟上线新代码是一样严肃的。
3. 实操过程与核心环节实现
这一节进入到能直接抄作业的部分。我会从搭建模板库目录、写一个可复用的渲染器、再到具体编排一个多 Agent 流程,逐步展开。代码我用 Python 和 Jinja2 做示例,因为这两样东西组合起来最简单,也最接近多数自研 Agent 项目的技术栈。
3.1 搭建一个可复用的提示词模板库
目录结构不要想得太复杂,从最小可用开始。我通常这么组织:
prompts/ ├── templates/ │ ├── customer_service/ │ │ ├── main.jinja2 │ │ ├── system.jinja2 │ │ └── variables.json │ ├── report_writer/ │ │ ├── main.jinja2 │ │ ├── system.jinja2 │ │ └── variables.json ├── rendered/ │ └── customer_service_20250601.txt └── loader.pytemplates目录下每个业务场景单独建一个文件夹,main.jinja2是完整提示词入口,system.jinja2存放系统指令,variables.json声明变量类型和默认值。rendered目录用来保存调试时渲染出来的最终提示词,这样和模型日志可以对照检查。
loader.py的作用是提供统一的加载接口。我给你写一个最基础的版本:
# loader.py from pathlib import Path import json from jinja2 import Environment, FileSystemLoader, StrictUndefined BASE_DIR = Path(__file__).parent env = Environment( loader=FileSystemLoader(str(BASE_DIR / "templates")), undefined=StrictUndefined, trim_blocks=True, lstrip_blocks=True, ) def load_template(template_name: str, variables: dict) -> str: tpl = env.get_template(f"{template_name}/main.jinja2") return tpl.render(**variables) def load_variables(template_name: str) -> dict: with open(BASE_DIR / "templates" / template_name / "variables.json", encoding="utf-8") as f: return json.load(f)注意我设置了StrictUndefined,这意味着一旦变量缺失,渲染会直接报错,而不是静默输出空字符串。这个配置非常关键,它能把“变量漏传”从隐蔽问题变成显性错误,省了我大量排查时间。
3.2 实现变量注入与条件渲染
新建一个customer_service/main.jinja2,内容大概长这样:
{% include "customer_service/system.jinja2" %} 用户问题:{{ question }} 用户类型:{{ user_type | default("普通用户") }} {% if is_urgent == true %} 注意:该用户已标记为紧急,请优先响应并提供联系方式。 {% endif %} {% if order_id %} 涉及订单:{{ order_id }} {% endif %} 输出要求: - 回答前先判断用户意图属于 {{ intents | join(", ") }} 中的哪一类。 - 如果无法回答,请直接回复“需要人工处理”,不要编造。模板里用了include引入系统指令,用了default过滤器处理可选变量,用了if条件渲染紧急信息和订单号。渲染代码只要调用load_template就行:
variables = { "question": "我的订单什么时候能到?", "user_type": "vip", "is_urgent": False, "order_id": "SO20250101", "intents": ["物流查询", "退换货", "发票"], } rendered = load_template("customer_service", variables) print(rendered)这里我要强调一个容易忽略的点:用户输入是可能包含特殊字符的。如果用户消息里恰好有{{或{%,Jinja2 会当成模板语法解析,导致渲染出错甚至注入模板逻辑。所以在把外部输入塞进模板之前,必须先转义。Jinja2 默认对变量内容转义,但前提是你渲染时用了{{ variable }}而不是直接拼接字符串。任何时候都不要用+拼接用户内容进模板,这是从安全角度必须遵守的底线。
3.3 编排 Agent 流程:一个包含规划、执行、反思的多智能体实例
模板库搭好之后,我们来看怎么用它编排一个多 Agent 流程。我用一个非常小的实例说明结构:一个 Planner Agent 拆解任务,一个 Executor Agent 执行并调用工具,一个 Critic Agent 检查和修正输出。它们共享同一个消息池,但每个 Agent 有独立的提示词模板。
先定义基础消息结构:
# agent_types.py from dataclasses import dataclass, field from typing import List @dataclass class AgentMessage: sender: str receiver: str content: str @dataclass class AgentState: task: str plan: str = "" execution_result: str = "" final_output: str = "" messages: List[AgentMessage] = field(default_factory=list)Planner 的提示词模板planner.jinja2可以写成:
你是任务规划者。根据用户任务,将任务拆解为 2 到 4 个可执行的步骤。 只输出步骤,不要解释理由。 用户任务:{{ task }} 输出格式: 1. 步骤描述 2. 步骤描述Executor 的模板executor.jinja2:
你是执行者。请完成如下步骤,如果步骤需要查询外部信息,可以使用 search 工具。 先获取工具结果,再给出最终回答。 当前步骤:{{ step }} {{ tool_result_block }}Critic 的模板critic.jinja2:
你是质量审校者。检查执行者给出的回答是否满足以下要求: 1. 是否直接回答了用户任务 2. 是否引入了编造的事实 3. 是否包含明确结论 如果不满足,请列出需要修正的地方;如果满足,请回复“通过”。 用户任务:{{ task }} 执行者回答:{{ execution_result }}运行编排的伪代码如下:
# orchestrator.py def run_agent(state: AgentState, models: dict): # 1. planner 生成计划 planner_prompt = load_template("planner", {"task": state.task}) state.plan = models["planner"].chat(planner_prompt) # 2. 按计划执行 steps = parse_steps(state.plan) for step in steps: tool_result = call_search_tool(step) if "查询" in step else "" executor_prompt = load_template("executor", { "step": step, "tool_result_block": f"工具返回:{tool_result}" if tool_result else "", }) state.execution_result += models["executor"].chat(executor_prompt) + "\n" # 3. critic 检查 critic_prompt = load_template("critic", { "task": state.task, "execution_result": state.execution_result, }) review = models["critic"].chat(critic_prompt) if review.strip() != "通过": state.final_output = f"需要修订:{review}" else: state.final_output = state.execution_result return state这只是一个最简示意。真实项目中你还需要加超时限制、重试逻辑、工具调用的错误处理。但核心思想是清楚的:每个 Agent 有明确的输入字段和输出字段,模板之间通过状态对象传递数据。这种结构比把所有逻辑写在一个大 prompt 里好维护得多。
3.4 接入 Dify/Coze 类平台时的编排映射
如果不想完全自研,用 Dify 或 Coze 也能实现类似效果。关键是理解平台节点和自研模板之间的映射关系。
| 自研模板字段 | Dify 节点 | Coze 对应能力 |
|---|---|---|
| system 指令 | LLM 节点的 System Prompt 或预设 Prompt | Bot 的 Persona & Prompt |
| 变量声明 | 节点输入变量,如{{#context#}}、{{input}} | 输入变量配置 |
| 条件片段 | IF/ELSE 分支节点 | Condition 节点 |
| 工具调用 | 工具节点 | 插件/工具 |
| 多 Agent 流程 | Agent 节点 + 多轮编排 | Workflow 多 Bot |
我在 Dify 里复刻上面的三 Agent 流程时,建了一个 planner 节点、一个 executor 节点、一个 critic 节点,节点之间用变量传递结果。Dify 的好处是你可以直接在界面上看到哪个节点挂了,变量值是什么,调试效率很高。但要注意平台模板的变量引用语法和 Jinja2 不完全一致,比如 Dify 用的是{{#node_id.output#}}这种引用方式。迁移的时候不要直接复制的模板内容,先用小例子确认语法差异。
另外,平台的编排一般只覆盖“流程顺序”,不一定覆盖 Agent 内部的思考循环。如果你需要模型在单节点内部自行多次调用工具,就要用 Agent 节点而不是普通 LLM 节点。这个区别很多新手会踩坑。
4. 常见问题与排查技巧实录
写模板和编排这块,问题往往在运行之后才暴露。我把这段时间积累的排查经验整理成几类典型问题,每一类都附了定位思路和解决方案,希望对你有直接帮助。
4.1 变量注入失败 / 模板渲染乱码
症状是模型回答里出现{{ variable }}原样输出,或者渲染后的提示词里变量位置是空的。大部分原因有三个:一是变量名拼写不一致,模板里写user_name,代码里传username;二是忘了设置 StrictUndefined,导致变量缺失时 Jinja2 静默渲染为空;三是编码问题,Windows 下偶尔会出现读取模板文件用 gbk 编码,中文乱码。
排查时先打印渲染前后的模板内容,这是最快的方式。写一行调试代码:
print("渲染前模板片段:", open("templates/customer_service/main.jinja2", encoding="utf-8").read()) print("渲染后结果:", load_template("customer_service", variables))如果渲染后没有报错但内容乱码,检查文件是否真的以 UTF-8 保存,Python 打开时也显式指定encoding="utf-8"。如果变量丢了,第一时间把所有变量清点一遍,对照variables.json里的声明,确保传入变量集合是声明的超集。
4.2 Agent 上下文溢出或遗忘
长任务跑到后面,模型开始忘记系统提示里的规则,或者前面的结论被后面的内容冲掉。这是 Agent 开发最普遍的问题。
我目前觉得最可靠的做法是“把最关键的指令放在开头和结尾”。很多大模型的注意力分布对两端更敏感,中间的容易被忽略。我在系统提示开头放“绝对禁止编造”,在每一步任务提示结尾放“如果信息不够,请直接说明”。这两个位置是最不容易被稀释的。同时在长任务里,不要让 Agent 一直背着一整段历史记录,而是定期把旧对话浓缩成摘要,只保留最近的原始上下文。
代码层面,可以在循环里加一个上下文管理函数:
def trim_context(messages, max_tokens=4000, summary_model=None): # 计算当前总长度 total = sum(count_tokens(m["content"]) for m in messages) if total <= max_tokens: return messages # 把最早的一半消息压缩成摘要,替代原始内容 old = messages[: len(messages) // 2] new = [{"role": "system", "content": f"历史摘要:{summarize(old, summary_model)}"}] + messages[len(messages) // 2:] return new摘要不是万能的,但能让你在有限上下文窗口内跑完更长任务。记住一个原则:原始上下文只保留关键最近的几轮,其余全部摘要化,系统提示永远保留第一位。
4.3 多智能体循环冲突
多个 Agent 协作时,最常见的问题是状态污染:Agent A 改了共享变量,Agent B 不知道,结果 B 基于过期信息继续跑;或者 Agent A 的内容被 B 当成最终结果,导致输出出现循环引用、无限往复。
我的解决方案是把共享状态做严格隔离。每个 Agent 只能读取自己声明的输入字段,输出字段写入自己的命名空间,例如planner.plan、executor.output。跨 Agent 读取必须显式声明依赖,比如 Critic 的输入是planner.plan和executor.output,把它写在variables.json里。这样代码一眼就能看出信息流向,出了问题也好定位。
另外,多 Agent 循环一定要设置最大迭代次数。我之前排查过一个问题,两个 Agent 互相挑毛病,你改我再改,整整调了一百多轮才被外部超时打断。现在我在所有循环入口都加一个固定上限,比如五轮,超过就强制终止并让最后一个 Critic 给出结论。宁可效果略差,也不能让任务吊死。
4.4 模板管理与 prompt 泄露风险
网上经常有“prompt 泄露”的新闻,很多人说是模型把系统提示输出了。这个问题确实存在,但缓解手段是可以做好的。
第一,不要把密钥、内部 API 地址、数据库信息写进模板,更不要写在系统提示里。所有机密信息应该通过工具调用去访问,而不是塞进上下文。第二,日志和调试工具里不要记录完整渲染后的 prompt,尤其是有用户输入的 prompt。我会对打印内容脱敏,把变量值替换成[REDACTED]。第三,用户输入可能包含“忽略上述指令”这种注入,最好的防法是让用户输入只出现在明确的变量区块,并且在渲染后做一个简单的异常检测,比如检测是否包含“忽略”、“system prompt”等关键词,命中的话直接把该次请求标记为高风险,走人工处理。
模板本身就是资产,泄露之后的损失不只是文本,还有你精心设计的角色逻辑和调优经验。我通常会做一套模板差异监控,如果生产环境的模板内容和 Git 仓库不一致,立刻告警。这套机制虽然简单,但真的能在出问题的时候第一时间发现。
5. 后续可以这样扩展
写完了模板管理和多 Agent 编排的基础实践,最后分享一个我正在用的方向:把提示词模板和自动评测体系打通。以前每次改模板,都要靠肉眼判断效果变化,后来我给每个模板配上评测用例集,每次渲染后用模型评分或人工抽查打分。这个做法让我在调整提示词时有了数据支撑,不再靠感觉。
如果你们正在搭建自己的提示词库,我的建议是从最小可用版本开始:一个场景、一个模板、一个变量文件,先跑通渲染和 Agent 循环,再慢慢扩展。模板管理最怕一开始就设计得特别宏大,结果维护成本把团队压垮。先把简单流程跑起来,让团队看到可复用的价值,后面自然会有动力填充更多模板。