ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

提示词模板与Agent编排实战:从复用资产到动态调度

提示词模板与Agent编排实战:从复用资产到动态调度 这里还是由我来回复吧。这句话有点危险我需要降低风险。 ## 1. 提示词模板从碎片化输入到可复用资产我在做 Agent 开发的前几个月一直处于一种每次写提示词都像重新发明轮子的状态。同一个场景的任务比如让模型从用户消息里抽取结构化信息我可能在十个不同的项目里写了十种风格完全不同的 prompt有的用 XML 标签有的用 Markdown 标题有的干脆就是几行自然语言。结果就是效果不稳定、调试成本高、换模型之后全部要重来。后来我把提示词模板这件事正经当成一个工程问题来处理整个开发节奏才真正顺起来。提示词模板的核心价值不是省几个 token或者少打几行字而是把 prompt 从一段一次性文本变成一份可以版本管理、可以测试、可以复用的资产。这跟写代码是一样的逻辑——你不会把同一段业务逻辑复制粘贴到十个地方你会抽成函数、做成模块。提示词模板就是 prompt 层面的函数封装。1.1 模板化之前先想清楚哪些部分在变很多人一上来就套 Jinja2 或者 Mustache 语法{{ variable }}写了一大堆结果模板变得比原来还难维护。我建议先做一个简单的静态分析把历史所有 prompt 拿出来看哪些部分每次都在变哪些部分基本上不动。变的部分通常是这几类用户输入对话内容、待处理文本、问题描述上下文数据数据库查询结果、工具返回的 JSON、外部 API 响应指令参数输出格式、语言、长度上限、风格要求示例数量few-shot 的样本条数、示例的详细程度不变的部分通常是角色设定基座你是一个擅长 X 的助手任务目标描述要完成什么、输出什么格式通用约束不要编造、只基于给定内容回答输出结构定义JSON Schema、字段说明把这两类分开模板的骨架就出来了。变的部分用变量占位不变的部分硬编码成模板的固定段落。这不是什么高深理论但很多人懒得做这一步直接凭感觉把一整段 prompt 复制来复制去最后改一个参数要在五个地方同步漏一个就出事。1.2 模板语法选型不要过度设计Jinja2 是 Python 生态里最常见的模板引擎功能强大到可以做条件判断和循环。但我要给一个忠告在提示词模板里尽量只用变量替换不要用复杂的控制语句。理由很实际大模型对 prompt 的敏感度很高结构上的轻微变化可能导致输出风格的漂移。如果你在模板里用{% if %}控制某段指令在特定条件下才出现那么同一个任务、不同请求之间 prompt 的差异会变大不利于稳定复现。而且模板引擎渲染出来的文本出了问题很难肉眼排查——因为渲染前模板是好的渲染后可能就变样了你还得手动渲染一遍才能看到实际发给模型的文本长什么样。我现在的做法是模板里只允许{{ variable }}任何需要条件判断的段落在 Python 代码层做好分支再传入不同的模板变量。这样每个模板都是确定性的同样的输入永远渲染出同样的 prompt。排查问题的时候我只需要把渲染结果打印出来一眼就能看出哪里不对。1.3 模板文件怎么组织目录结构也是一种设计模板多了之后文件组织就成了一个问题。我自己用的一套结构是这样的prompts/ ├── common/ │ ├── role_base.md │ └── output_constraints.md ├── extractors/ │ ├── user_intent.md │ ├── entity_extraction.md │ └── sentiment_analysis.md ├── generators/ │ ├── report_writer.md │ ├── email_reply.md │ └── code_reviewer.md └── agents/ ├── research_agent.md ├── coder_agent.md └── planner_agent.md每个子目录代表一类任务文件就是具体的 prompt 模板。common目录放的是所有模板都会引用的公共段落——角色设定基座、通用输出约束这种。通过模板引擎的 include 机制在具体模板里拼进来。这套结构的价值在于当你想调整所有模板都必须遵守的输出规范时只需要改common/output_constraints.md一个文件而不是去翻十几个 prompt 逐个改。项目跑了大半年我越来越确定一件事——提示词模板的管理难度跟代码项目一样遵循高内聚低耦合原则公共部分抽得越干净后期维护越轻松。1.4 版本管理与回归测试提示词模板一旦开始被多个 Agent 引用就必须纳入版本管理。我在每个模板文件头部加了一段注释式的元信息!-- id: coder_agent_v3 -- !-- parent: v2 -- !-- changed: 调整了代码审查的输出格式新增复杂度评分字段 -- !-- models: gpt-4o, claude-3.5-sonnet --这段元信息不参与模板渲染只是给人看的。我习惯把模板文件放在 Git 仓库里每次修改都提交commit message 写清楚改了什么、为什么改。这样当某个 Agent 突然效果变差时我可以通过git log查看最近的模板变更快速定位是不是 prompt 改动引起的。回归测试这块我做的也比较简单每个模板配一个示例输入和期望输出的测试用例用脚本批量跑一遍看输出是否符合预期。因为 LLM 输出本身有随机性我不会做严格的断言而是检查关键字段是否存在、输出格式是否合法、有没有出现已知的坏模式比如作为一个人工智能助手这种废话开头。这套回归测试救了我好几次尤其是我同时维护多个 Agent 的时候改一个公共模板其他 Agent 可能集体受影响不跑一遍根本发现不了。2. Agent 提示词编排为什么单条 prompt 不够了模板解决的是单条提示词怎么复用的问题但 Agent 场景下模型不是只跑一次就结束。一个稍微完整点的 Agent可能要经历规划、调用工具、读取结果、修正方案、再次执行等多个轮次。每个轮次可能都有不同的 prompt 需求而且轮次之间还要传递上下文状态。这就是标题里说的剪辑——把多条提示词按流程编排起来让模型或模型组在一个大任务里协同工作。2.1 Agent 提示词编排的核心矛盾单轮对话里prompt 是静态的给模型一段完整的指令模型返回结果完事。但 Agent 的循环是动态的模型决定调什么工具工具返回结果模型根据结果再决定下一步。这意味着 prompt 不能一次性写完必须分阶段、按需生成。我在实际开发中遇到的最典型问题就是提示词上下文爆炸把每个环节的指令、历史记录、工具输出全部拼进一个超长 prompt模型倒是能处理但效果越来越差。原因不复杂——上下文越长模型对关键指令的注意力就越容易被稀释尤其在长上下文窗口里模型经常出现忘掉开头指令的问题。所以编排的核心不是把 prompt 拼得越长越完整而是在合适的时机给模型喂合适的指令片段。这跟做菜差不多——你不会把所有食材一口气全倒进锅里而是按顺序下锅该爆香的爆香该炖的炖最后出锅。2.2 分层编排System Prompt / Task Prompt / Data Context我把 Agent 提示词的编排分成三个层次第一层是 System Prompt系统角色层。这一层承载 Agent 的人格和世界观——它是谁、擅长什么、有什么底线。System Prompt 在 Agent 整个生命周期里基本不变属于最高优先级的固定指令。第二层是 Task Prompt任务指令层。这一层承载当前这一步要做什么是动态生成的取决于 Agent 当前的执行阶段。比如规划阶段的任务指令是请列出完成目标的步骤调用工具后是请基于工具返回结果判断下一步。Task Prompt 需要针对不同阶段单独设计不能一套话通吃。第三层是 Data Context数据上下文。这一层承载的是任务相关的数据——用户输入、历史消息、工具返回结果以结构化的方式塞进 prompt。数据上下文最容易被写乱常见问题是不做筛选全量拼接舍不得丢任何信息。我在编排时的思路是System Prompt 负责稳定Task Prompt 负责引导Data Context 负责供给。三层各司其职不该混淆的绝不放一起。具体到代码层面这三层分别由不同的函数生成最后再拼接成一条完整的 prompt 发给模型。2.3 动态指令生成的两种模式模式一阶段静态指令。每个阶段配一段写死的 Task Prompt根据 Agent 状态切换。模式二动态生成指令。让模型自己总结下一步做什么然后基于总结生成指令。这两个模式我都在生产环境里用过。阶段静态指令稳定性高、成本低适合流程固定的场景比如先抽取实体再调用 API最后生成报告。动态生成指令灵活性强适合开放性的任务比如研究型 Agent它每一步做什么完全取决于当前发现的线索。实际项目中我倾向先固定流程因为动态生成看着酷但失控风险高——模型可能自己东拐西拐绕半天回不到目标。等流程跑稳了再逐步放开自由度。这个顺序反过来就是给自己挖坑我也踩过乱写一气生成的指令让 Agent 直接进入鬼打墙状态。2.4 多 Agent 协作时的提示词编排多个 Agent 协作的场景下每个 Agent 跟其他 Agent 交互的接口 prompt 要单独设计。拿经典的研究型 Agent 组合为例一个 Planner 负责拆解任务一个 Researcher 负责检索资料一个 Writer 负责整理产出。Planner 输出的计划要能被 Researcher 理解Researcher 返回的资料要能被 Writer 消化。这时候关键的提示词编排点在于Agent 之间的消息传递格式。我习惯让前一个 Agent 的输出明确包含给下游 Agent 的指令性文本和纯数据内容两部分用分隔符隔开。下游 Agent 的 Task Prompt 里明确指示它该关注哪部分。用大白话说就是每个 Agent 不光活着还得知道自己接的是谁的活儿、干完交给谁。这个设计的投入产出比极高——多 Agent 协作中的乱七八糟问题大半出在不明确的接口上。3. 实操一个带记忆和工具调用的 Agent 提示词编排示例说点能直接用的。我拿一个具体的例子展示整个提示词模板管理和编排过程。场景是做一个代码审查 Agent接收用户提交的代码片段和审查要求先分析代码问题再按需调用一个复杂度计算工具最后输出结构化的审查报告。3.1 准备阶段定义工具与接口工具就是一段可以被模型调用的函数描述包含名称、参数、返回结构等元信息。比如我的复杂度计算工具定义如下# 工具接口定义 def cyclomatic_complexity(code: str) - dict: 计算代码圈复杂度。 参数 code (str): 需要分析的代码片段 返回 dict: { complexity: 整数, risk_level: low | medium | high, top_functions: [函数名列表] } # 实际实现... return result为了让模型能正确调用这个工具我需要把工具的描述转成模型能理解的形式塞进 prompt。对于 OpenAI 系模型用 function calling 接口对于通用场景把工具描述以一个代码块放进 Task Prompt效果也够用。3.2 模板设计System / First Step / Tool Result 三件套整个 Agent 用三段模板第一段 System Prompt你是一个资深代码审查专家。你会收到一段源代码和审查要求。 你的输出必须遵循 JSON 格式包含 findings问题列表、 suggestions修改建议、complexity_report复杂度报告。 如果用户没有明确要求默认审查代码风格、潜在 bug、安全风险和可读性。 你在审查前必须调用复杂度计算工具来辅助判断。第二段是第一步的 Task Prompt——拿到用户代码后先分析用户提交的代码 {{ code }} 审查要求 {{ user_requirement }} 请先调用复杂度计算工具获得复杂度数据后再结合你自己的静态分析 输出完整的 JSON 审查报告。报告中每个问题必须包含 - severity: high/medium/low - line: 行号 - description: 问题描述 - suggestion: 修改建议第三段是工具调用返回结果后的 Task Prompt——注意这里的提示词要明确告诉模型之前让干啥还没变以及对工具结果怎么用工具返回结果 {{ tool_result }} 请基于工具返回的复杂度数据继续执行你的人工审查任务。 将复杂度数据补充到最终报告中并根据风险等级额外给出 针对高复杂度模块的重构建议。这三段拼起来就是一个最小可用的带工具调用 Agent 提示词编排。完整流程中Agent 会先调用工具拿到结果后再完成剩余工作。这个例子里我用一个用户问题映射流程串联起来先看输入结构决定输出结构再决定工具怎么用。3.3 编排逻辑的代码实现核心代码不复杂关键是结构清晰import json from typing import Dict, Any class CodeReviewAgent: def __init__(self, system_prompt: str, templates: Dict[str, str]): self.system_prompt system_prompt self.templates templates self.conversation_history [] def run(self, code: str, user_requirement: str, tool_executor: Any) - Dict[str, Any]: # 先将用户请求加入历史 self.conversation_history.append({ role: user, content: f审查以下代码:\n{code}\n要求:{user_requirement} }) # 第一步带上任务提示词通知模型去调用工具 first_messages [{role: system, content: self.system_prompt}] first_messages.append({ role: user, content: self.templates[first_task].render( codecode, user_requirementuser_requirement ) }) first_response yield from self._call_model(first_messages) # 识别模型工具调用请求并执行工具 tool_call first_response.get(tool_calls, [None])[0] if tool_call is None: raise ValueError(模型没有按预期调用工具) tool_name tool_call[function][name] args json.loads(tool_call[function][arguments]) tool_result tool_executor(tool_name, args) # 第二步把工具结果交给模型让它完成报告 second_messages first_messages [ {role: assistant, content: first_response[content]}, {role: tool, name: tool_name, content: json.dumps(tool_result)}, { role: user, content: self.templates[tool_result_task].render( tool_resultjson.dumps(tool_result) ) } ] final_response yield from self._call_model(second_messages) return final_response[content]这段代码有几个细节值得注意第一system_prompt 在两次模型调用中保持一致保证了 Agent 的人设和基本约束不飘。第二第一次调用后我把 assistant 的回复作为中间状态写入第二次调用给模型保留了我说过要调用工具的上下文——丢掉这段模型很可能在第二次调用时以为自己刚开始干活。第三工具结果通过一个独立的 user 消息塞回去而不是直接拼接在 system prompt 里避免污染固定指令。3.4 测试与调试渲染结果检查我在开发这个 Agent 时最常用的调试手段是打印渲染后的完整 prompt。很多提示词问题代码层看不出来渲染出来一眼就见分晓。比如有一次我发现模型总是忽略工具返回结果排查了半天最后打印 prompt 才发现工具结果被模板引擎渲染成了 HTML 转义格式JSON 里的全变成了lt;gt;模型当然读不懂。换用自定义的渲染函数解决。再分享一个经验模板变量命名要反直觉一点。我见过有人把用户输入的代码片段命名为text把用户审查要求命名为input这种泛化命名在复杂模板里特别容易看岔。我偏好长一点的、能表达语义的命名source_code、review_requirements。渲染时模板格式干净定位问题时揪着变量名一眼就能找到理解错的地方。4. 常见问题与排查技巧实录提示词编排在 Agent 项目里踩坑率高的点其实集中在那几个不透明的环节。下面的问题全是我自己或团队在真实项目里碰到过的把排查思路一并写出来。4.1 模型不按预期调用工具症状Agent 在处理任务时完全不调用工具或者明明该调工具却输出一段文字假装调用了。排查顺序工具描述是否足够明确工具名、参数名、返回值结构是否自解释。我在工具描述里习惯加一句如果 XX 条件满足必须先调用这个工具再回答。System Prompt 是否和 Task Prompt 冲突如果 System Prompt 说你必须调用工具Task Prompt 说直接回答模型会困惑行为随机。模型是否真的支持工具调用接口有些模型走文本生成接口对工具调用格式理解较弱需要换成带tool_calls的输出结构或使用专门微调过的模型。4.2 模型调用了工具但参数传错症状工具调用成功但传进去的参数是空字符串、错误字段名或者多余的字段。排查思路工具参数名和任务提示词里出现的字段名要一一对应。模型很容易照着提示词里的说法填参数名我在提示词里提到的字段名尽量和工具接口字段名保持一致。工具参数的 JSON Schema 定义要严格。required字段设对type字段设对枚举值范围写清楚。模型在 schema 约束下更不容易跑偏。对参数做一层校验失败时把校验错误返回给模型让它修正。这一步能大大提升稳定性。4.3 上下文越传越长效果越来越差症状Agent 早期表现不错跑一段时间后开始重复输出、遗漏指令、生成无关内容。原因分析多层 prompt 拼接后历史消息、工具结果、中间状态全堆在上下文里。指令文本被海量数据淹没。我用的对策对历史消息做滑动窗口裁剪只保留最近 N 轮和任务目标相关的信息。对工具结果做摘要化处理再放回上下文而不是把原始 JSON 全塞进去。大 JSON 对模型理解负担重模型被迫在噪音里挑信号。在 Task Prompt 里重新强调当前目标和输出结构给模型明确锚点。4.4 模板渲染后出现意外字符症状prompt 渲染后出现多余的空格、换行、注释符号或者变量值被转义。排查用 repr 或 json.dumps 打印渲染结果肉眼检查每个字符。Jinja2 模板里比较隐蔽的问题是trim_blocks和lstrip_blocks的设置开了和不开渲染结果完全不同。反复试几次然后把设置写死在配置里不要依赖默认值。4.5 Docker 部署场景下的提示词路径问题这个问题在 Agent 项目部署时容易被忽视提示词模板文件放哪里、路径怎么找、容器里能不能读到。我的做法是模板路径不要用相对路径统一用一个配置项指定模板根目录运行时拼接具体文件路径。Docker 镜像构建时显式COPY模板目录不要靠挂载卷——否则容器重启时模板文件缺失Agent 报错都报得很莫名。还有一个坑是某些容器环境把模板目录和代码目录分开放代码找模板的路径要写对最好在启动时加一个自检检查所有模板文件是否存在。4.6 多 Agent 编排消息阻塞症状多个 Agent 协作时一个 Agent 等待另一个的结果系统卡住或超时。排查Agent 编排过程中消息传递是异步还是同步流我只用过同步串行但这要求每个 Agent 步骤结束时必须明确产生可用于传递的状态对象否则下一个 Agent 拿不到输入。实战中我遇到过下游 Agent 的 Task Prompt 要求它从 {context} 获取信息但 {context} 是空的——因为上游 Agent 输出格式变了字段名对不上。多 Agent 场景中Agent 之间的接口契约要固定并且用统一的 Pydantic 模型验证千万别图方便传 dict 裸数据。5. 工具与框架选型从手写编排到框架上面的示例是手写实现代码还能精简但完整性高适合理解原理。生产项目里我建议考虑成熟框架。这里说几个我在实际项目里验证过的选型思路不偏袒谁纯粹讲更适合什么场景。5.1 LangChain 系灵活但容易过度抽象LangChain 的提示词模板PromptTemplate和 Agent 编排AgentExecutor是一套相对完整的东西。优点是对接模型、工具、记忆的封装比较全写起来省事。缺点是抽象层有点厚出问题时排查链条长——模板、链、代理、工具缓存、记忆组件每个环节都可能出问题查起来比手写麻烦。我建议在项目早期或原型阶段用它快速验证正式进生产前把核心流程用自己的代码重写一遍只保留 model 调用和工具执行两个边界。这句话可能说得比较得罪人但是代码库依赖越重、抽象层越多Agent 项目的上线周期越长。5.2 轻量框架保持透明如果项目对可控性要求高我用过一段时间的轻量级方案只用 OpenAI SDK 或 Claude SDK 的原生工具调用接口绕开框架自己做编排。代码结构并不是特别多多系统替换、多轮调度逻辑加起来也就几百行。优点是每一步 prompt 都是我自己生成的渲染结果随时可查排查什么问题都在自己代码里找原因。缺点是逻辑代码没有复用性换个任务要改一遍。这个抉择没有标准答案。我的经验是如果你在做一个 Agent 平台或通用框架值得投资一个编排层如果你只是做某个具体业务场景的 Agent手写比框架快得多。手写成本看起来高但维护成本低因为它少了一堆你不知道它干什么用的层级。5.3 模板引擎之外的模板管理模板量大到一定程度文件系统的目录结构会开始吃紧比如跨项目复用模板、多人协作编辑同一份模板。这时候我会上模板版本管理工具——不是git 管文件那个级别而是像 i18n 或翻译记忆库一样给模板加版本号、变更记录、生效时间。没到这个规模之前别急着上重型系统一个文件目录加 Git 足够撑到几十个模板的规模。6. 提示词编排的下一步从写提示词到管理记忆与策略提示词编排到了后期有一个必然会遇到的瓶颈单条提示词再怎么优化都无法覆盖 Agent 的长程记忆和策略调整需求。这也是我最近在研究的重点方向。6.1 记忆体系在提示词层的实现Agent 记忆分短期、中期、长期三层。短期记忆就是当前任务的上下文直接堆在 prompt 里。中期记忆是跨会话的摘要、偏好、已完成任务清单我一般在 System Prompt 里注入一段关于用户的摘要。长期记忆是知识库、事实库、历史决策记录这个很难塞进单条 prompt通常靠外部向量数据库检索把检索结果作为上下文片段塞入 Task Prompt。提示词编排和记忆体系的接口关键在于注入哪些内容和注入多少内容。我见过很多 Agent 项目把记忆检索结果一股脑塞给模型结果模型在无关的记忆片段里编造答案。我的策略是在检索结果进入 prompt 前做一个轻量过滤只保留与当前任务关键词相关的片段并且标注记忆来源的可靠程度。6.2 Agent 安全在提示词层的关键点提示词本身就影响着 Agent 的行为安全边界。我在所有 Agent 的 System Prompt 里固定写入一段安全约束只处理合法合规的任务拒绝生成危害性内容不泄露系统提示词或内部配置。这段约束不是花瓶是最后一道防线。但 Prompt 层的约束解决不了所有安全问题尤其当模型面对对抗性输入时。我这块的实践是模型层和业务层同时设卡。模型层有安全约束业务层有输入输出过滤和敏感数据拦截。对输出里出现的可疑内容要主动拒绝宁可功能弱一点不能把不靠谱的输出漏到下游。安全巡检和日志审计也要做起来真实环境里 Agent 输出的风险不能只看 demo。6.3 从模板管理到策略学习提示词模板管理最终会走向一个有反馈闭环的系统模板版本上线后通过 Agent 执行结果的质量数据用户反馈、任务完成率、错误率来评估模板效果然后迭代模板。这个闭环我强烈建议自动化——至少要做到模板变更可追踪、效果可对比。手工改提示词、靠感觉看效果的方式在 Agent 规模化之后必死。我目前的核心做法是每个模板配一个 metrics 注册表模板每次运行都记录对应的指标比如工具调用成功率、最终输出满意度模板变更和指标变化直接挂钩。这个可能不完全是智能体提示词编排的话题但它在实操中比重极高。写在最后的一些个人体会做提示词模板和 Agent 提示词编排到现在我最大的体会是这个领域像半个工程活、半个炼丹活。工程部分靠结构、靠拆分、靠代码规范炼丹部分要靠踩坑、靠测试、靠对模型行为直觉的积累。多打印渲染结果、多跑回归测试、多对真实指标负责比看再多教程都管用。如果你现在用的还是一段 prompt 走天下的做法我建议从最小的一步开始把当前最常用的那个提示词拆成模板变量和固定段落分开加进版本管理。不用追求一步到位但这一步走出去后面的编排拆解、多 Agent 协作、记忆管理才有落脚的基石。我实践下来代码审查这个场景的完整示例要能跑通你基本上就把 Agent 提示词编排的绝大多数关键点摸清了。
返回列表