1. 从“鸡同鸭讲”说起为什么你的Agent总在答非所问做过Agent项目的人大概率都经历过这种崩溃瞬间你精心设计了一套多轮对话流程接入了能力不错的大模型结果用户问“帮我看看这个订单还能不能改地址”Agent却一本正经地回复了一段关于退货政策的说明。用户觉得它蠢你觉得委屈——明明模型能力没问题流程也跑通了怎么就是“聊不到一块去”问题的根子往往不在模型本身而在Prompt和Response之间的那道“转换鸿沟”。用户说的话是自然语言带着省略、指代、情绪和隐含意图Agent内部执行需要的是结构化、无歧义、可被工具链消费的指令。这两者之间如果没有一个专门的转换层就会出现典型的“鸡同鸭讲”用户说的是AAgent理解成B执行了C最后回复了D。这篇要聊的Prompt/Response Optimiser就是专门解决这个问题的架构组件。它在整个Agent体系里扮演的角色类似于一个“同声传译意图校准”的中间人——把用户的模糊表达翻译成精确的Prompt再把模型返回的原始Response翻译回用户能理解、系统能消费的格式。我把它放在Agent架构模式系列的第三篇是因为前两篇讲完整体分层和记忆机制之后这个组件是决定Agent“聪不聪明”的最关键一环。适合谁看如果你正在做基于基础模型的Agent应用已经跑通了基本链路但发现对话质量不稳定、意图识别时好时坏、多轮之后上下文漂移严重那这篇就是写给你的。如果你还在选型阶段也可以提前了解这个组件该在什么位置、承担什么职责避免后期返工。2. 架构定位Optimiser到底该放在哪一层2.1 它在Agent流水线中的精确位置先把位置说清楚不然后面聊实现容易乱。一个典型的Agent处理链路大致是这样的用户输入 → 输入预处理 →Prompt Optimiser→ 模型推理 →Response Optimiser→ 输出后处理 → 用户注意我把Prompt Optimiser和Response Optimiser拆成了两个独立环节虽然标题里把它们合在一起叫“Prompt/Response Optimiser”但在实际工程中它们往往是两个可以独立部署、独立迭代的模块。合在一起讲是因为它们共享同一套核心逻辑——语义对齐。Prompt Optimiser负责“进去”的方向把用户的原始输入结合当前对话历史、系统状态、可用工具列表重写成一个模型最容易理解、最不容易产生歧义的Prompt。Response Optimiser负责“出来”的方向把模型返回的原始文本解析成结构化数据校验其是否符合预期格式必要时触发重试或降级最后再渲染成用户友好的表达。这两个模块之间隔着模型推理但它们的设计必须联动。我见过不少项目只做了Prompt侧的优化Response侧直接透传结果模型返回的JSON里多了一个换行符就导致整个解析崩溃。这种坑后面会细说。2.2 为什么不能把优化逻辑塞进模型本身有人会问现在模型能力这么强我直接把优化指令写进System Prompt里不就行了为什么还要单独搞一个组件这个问题我早期也纠结过。实测下来的结论是模型能做的事和工程该做的事边界要划清楚。把优化逻辑全部塞进System Prompt短期看省事长期看会带来三个致命问题。第一是不可观测。优化逻辑藏在Prompt里你没法单独统计“有多少次用户输入被正确重写了”“有多少次Response解析失败了”。出了问题只能靠猜这在生产环境里是灾难。第二是不可迭代。你想调整优化策略就得改System Prompt而改System Prompt又可能影响模型在其他任务上的表现牵一发动全身。独立组件就可以单独发版、单独A/B测试。第三是成本不可控。每次请求都让模型做一遍优化Token消耗是实打实的。独立组件可以在本地做规则匹配、缓存命中、轻量模型预处理把大模型的调用留给真正需要它的环节。提示如果你的Agent日请求量在千次以下把优化逻辑放进System Prompt确实更省事。但一旦上量独立组件的收益会迅速超过它的维护成本。2.3 三种常见的部署形态对比实际落地时Optimiser有三种常见的部署形态各有适用场景形态实现方式延迟适用场景维护成本内联式作为函数嵌入主流程极低原型验证、低频调用低边车式独立服务主流程调用中等中高频、需要独立扩缩容中流水线式多级Optimiser串联较高复杂意图、多工具场景高我个人的建议是从内联式起步跑通验证后再根据瓶颈决定是否拆成边车。不要一上来就搞流水线那是在给自己挖坑。很多团队在日请求量还没过万的时候就开始设计多级优化流水线结果大部分环节的命中率极低纯粹是过度设计。3. Prompt Optimiser把“人话”翻译成“模型话”3.1 输入重写的四个核心动作Prompt Optimiser的核心工作可以拆成四个动作我按执行顺序来说。第一个动作是意图归一化。用户说“这玩意儿咋退”“我想把这个退了”“退货流程是啥”这三句话在语义上高度接近但字面差异很大。意图归一化就是把这些表达映射到同一个意图标签上比如intent: return_goods。这一步通常用轻量分类模型或规则关键词匹配来做不需要动用大模型。第二个动作是槽位填充。识别出意图之后要把用户表达里的关键信息抽取出来填进预定义的槽位。比如退货意图需要order_id、reason、preferred_time这几个槽位。用户可能只说“我要退昨天那个”那order_id就需要从对话历史里回溯补全。这一步是Prompt Optimiser里最容易出错的环节后面会专门讲。第三个动作是上下文压缩。多轮对话到第五轮以后历史消息会变得很长。全部塞进Prompt既浪费Token又容易让模型分心。压缩策略通常是保留最近N轮原文更早的轮次只保留摘要或关键实体。这里有个经验值最近3轮保留原文第4到第8轮保留摘要8轮以前只保留实体列表。这个比例不是拍脑袋定的是实测下来在信息保留率和Token成本之间的一个较好平衡点。第四个动作是格式封装。把归一化后的意图、填充好的槽位、压缩后的上下文按照模型最容易理解的格式组装成最终Prompt。这里有个细节不同模型对格式的敏感度不一样。有些模型对JSON格式的指令遵循度更高有些则对Markdown列表更友好。这个需要针对你实际使用的模型做A/B测试。3.2 槽位填充的三种策略与选择依据槽位填充是Prompt Optimiser里最考验工程能力的部分。我把它分成三种策略策略一规则优先。对于格式固定的信息订单号、日期、金额直接用正则表达式提取。优点是快、准、零成本缺点是无法处理变体表达。比如“昨天”“前天”“上周三”这种相对时间正则就很难覆盖全。策略二模型抽取。把用户输入和槽位定义一起发给模型让它输出结构化结果。优点是泛化能力强缺点是每次都要调用模型成本和延迟都上去了。策略三混合模式。先用规则跑一遍能填的槽位直接填剩下的空槽位再用模型抽取。这是我在生产环境里最推荐的方案。实测下来规则能覆盖大约60%到70%的槽位填充需求剩下的30%到40%交给模型整体成本和延迟都能控制在可接受范围内。注意槽位填充一定要做置信度标记。规则填充的槽位标记为高置信模型填充的标记为中等置信从历史回溯补全的标记为低置信。低置信槽位在后续流程中要触发二次确认不能直接拿去执行。3.3 上下文压缩的实操参数与踩坑记录上下文压缩这块我踩过的坑最多说几个具体的。第一个坑是摘要丢失关键否定词。早期我用模型做历史摘要结果“用户表示不需要退款”被摘要成“用户提及退款”语义完全反了。后来改成摘要时必须保留否定词和条件词并且在摘要Prompt里明确要求“不得改变原意”。第二个坑是实体列表膨胀。8轮以前只保留实体列表听起来很美好但如果用户前面提到了十几个商品实体列表本身就很长了。后来加了一个规则实体列表超过10个时只保留最近被引用的5个其余折叠为“等N个商品”。第三个坑是压缩时机。一开始我是每轮都压缩结果发现压缩本身也要调用模型成本反而更高了。后来改成滑动窗口触发式压缩只有当历史Token数超过阈值比如2000时才触发一次压缩压缩后Token降到阈值以下等再次超过时再压缩。这样压缩频率大幅降低整体成本反而更优。具体的压缩参数我整理成了一张表参数建议值说明原文保留轮数3最近3轮完整保留摘要保留轮数5第4到第8轮保留摘要实体保留数量5超出部分折叠压缩触发阈值2000 Token超过才触发压缩目标值1200 Token压缩后目标这套参数在我的场景里跑了半年多稳定性不错。但要注意这些值不是通用的你需要根据自己的对话平均轮数、单轮Token量做调整。调整方法很简单先记录一周的实际Token分布找到P90分位值把触发阈值设在P90附近即可。4. Response Optimiser把“模型话”翻译回“人话”4.1 结构化解析与容错处理Response Optimiser的第一道关卡是解析。模型返回的原始文本可能是纯文本、JSON、Markdown甚至是混合格式。解析器的核心任务是从非结构化输出中稳定提取结构化信息。这里最大的坑是模型输出的JSON不合法。常见的不合法情况包括多了一个尾逗号、字符串里没转义引号、用了单引号而不是双引号、嵌套层级过深导致截断。我试过直接json.loads失败率在早期能到15%左右这在生产环境是不可接受的。后来我加了三层容错第一层是预处理清洗。用正则把常见的格式问题修掉比如去掉尾逗号、把单引号替换成双引号、补全被截断的括号。这一层能解决大约80%的不合法情况。第二层是宽松解析。用支持注释和尾逗号的解析器比如Python的json5库再试一次。这一层能再解决10%左右。第三层是降级提取。如果前两层都失败就用正则从文本里直接抓取关键字段。这一层是兜底准确率会下降但至少不会让整个流程崩溃。三层下来解析成功率能稳定在99.5%以上。剩下的0.5%会触发重试或转人工这个比例是可以接受的。4.2 语义校验模型说的和用户问的是不是一回事解析成功不代表回答正确。Response Optimiser还要做一层语义校验确认模型返回的内容确实回应了用户的意图。校验的核心逻辑是意图-响应匹配度计算。把用户的意图标签和模型返回的内容做一次相似度比对低于阈值就判定为“答非所问”触发重试或降级回复。这里有个细节相似度阈值不能设太高。设太高会导致大量正常回复被误判用户体验反而变差。我的经验值是0.65到0.75之间具体取值要看你的意图分类粒度。意图分得越细阈值可以设得越高意图分得粗阈值就要相应降低。除了相似度还要校验槽位一致性。比如用户问的是订单A的状态模型回复里提到的却是订单B这就是槽位不一致。这种错误在多订单场景下特别常见必须单独校验。4.3 表达渲染让回复有“人味”的三个技巧校验通过之后最后一步是把结构化结果渲染成用户能看懂的表达。这一步做得好不好直接决定用户觉得Agent“聪明”还是“机械”。技巧一根据用户情绪调整语气。用户如果表达出焦急情绪“急”“快点”“怎么回事”回复要更直接、更短、先给结论。用户如果语气平和可以适当展开说明。情绪识别可以用关键词标点符号感叹号、问号密度来做不需要动用模型。技巧二控制信息密度。一次回复里不要塞超过三个要点。超过三个用户记不住体验反而下降。如果确实有更多信息拆成多轮回复或者用“还有两点需要我继续说吗”的方式让用户控制节奏。技巧三保留必要的“不确定表达”。模型不是万能的有些问题它确实不确定。这时候不要强行给一个肯定答案而是用“根据目前的信息可能是……建议你进一步确认”这样的表达。这比给一个错误答案然后被用户发现要好得多。5. 联动机制两个Optimiser如何配合5.1 共享上下文与状态同步Prompt Optimiser和Response Optimiser虽然分处模型两侧但它们必须共享同一份上下文状态。这份状态至少包含当前意图标签、已填充槽位、置信度标记、对话轮次、历史压缩摘要。状态同步的关键是版本一致性。Prompt Optimiser在重写输入时会更新状态版本号Response Optimiser在校验输出时要检查版本号是否匹配。如果不匹配说明中间有并发请求干扰需要重新拉取最新状态。这个机制在单用户单会话场景下看起来多余但在多用户共享Agent实例、或者用户快速连续发消息的场景下能避免大量状态错乱问题。5.2 反馈闭环Response如何反哺PromptResponse Optimiser的校验结果应该反哺给Prompt Optimiser形成闭环。具体来说如果某类意图的响应校验失败率持续偏高说明Prompt Optimiser在这类意图上的重写策略有问题需要调整。如果某类槽位的填充置信度持续偏低说明抽取策略需要优化。这个闭环不需要实时每天跑一次批处理统计就够了。统计维度包括各意图的响应校验通过率、各槽位的填充置信度分布、解析失败的类型分布。根据统计结果每周做一次策略调优。我自己的项目里这个闭环跑了一个月之后整体响应校验通过率从78%提升到了94%。提升主要来自两个方面一是发现了几个意图的重写模板有问题二是发现某些槽位的正则规则覆盖不全。5.3 性能与成本的平衡点两个Optimiser都会引入额外的延迟和成本。Prompt Optimiser的规则部分几乎零成本模型抽取部分每次调用大约增加200到500毫秒。Response Optimiser的解析和校验基本是本地计算延迟在50毫秒以内但如果触发重试就会再走一遍模型推理成本翻倍。平衡点的找法先统计重试率。如果重试率低于5%说明当前策略基本可用不需要大改。如果重试率高于15%说明Prompt Optimiser的重写质量有问题优化重点应该放在输入端而不是在输出端反复重试。我见过一些团队Response校验失败就无脑重试结果重试率飙到30%以上成本和延迟都爆炸。正确的做法是先修输入端再控输出端。输入端修好了重试率自然就下来了。6. 常见问题与排查技巧实录6.1 意图识别漂移的排查路径意图识别漂移是指同一类用户表达在不同轮次被识别成不同意图。这个问题在多轮对话里特别常见排查起来也有章可循。第一步检查上下文压缩是否丢失了关键信息。很多时候漂移是因为压缩把决定意图的关键词压掉了。把压缩关掉再跑一遍如果漂移消失那就是压缩策略的问题。第二步检查槽位填充是否污染了意图判断。有些实现会把已填充的槽位拼进意图分类的输入里如果槽位填错了意图也跟着错。把槽位和意图分类解耦分别独立判断。第三步检查意图分类的阈值设置。如果阈值设得太低稍微有点相似的表达就会被归到同一意图设得太高同类表达又会被分到不同意图。这个需要根据实际数据做ROC曲线来定。6.2 响应格式崩溃的五种典型场景响应格式崩溃是Response Optimiser最常处理的问题。我整理了五种典型场景和对应的处理方式场景表现处理方式尾逗号JSON末尾多逗号正则清洗引号未转义字符串内含未转义引号预处理替换嵌套截断输出被Token限制截断补全括号或降级格式混用JSON里混了Markdown分段解析编码异常出现乱码字符编码检测与转换这五种场景覆盖了我遇到过的90%以上的格式问题。剩下的10%通常是模型本身出了异常比如返回了空内容或者重复内容这种直接触发重试即可。6.3 多轮对话中的指代消解难题指代消解是Prompt Optimiser里最难的部分。用户说“把它改成明天”这个“它”指什么“明天”是相对于哪个基准时间都需要从上下文里推断。我的处理策略是维护一个实体栈。每轮对话中出现的实体按时间顺序压入栈中指代词它、这个、那个默认指向栈顶实体。如果栈顶实体不明确比如有多个候选就触发澄清询问而不是猜测。这个策略的准确率大约在85%左右剩下的15%需要通过澄清询问来解决。不要试图做到100%准确那是不可能的而且成本极高。85%的自动消解加上15%的澄清询问整体体验已经比全部澄清要好得多。提示澄清询问的话术也有讲究。不要问“你指的是哪个”而要给出具体选项“你是指订单A还是订单B”这样用户回答起来更省力整体轮次也更少。6.4 性能瓶颈的定位与优化Optimiser引入的性能瓶颈通常出现在三个位置Prompt重写的模型调用、Response解析的复杂正则、状态同步的锁竞争。定位方法很简单在每个环节打点计时跑一周的统计数据看P99延迟落在哪个环节。如果是Prompt重写慢考虑把模型抽取改成异步或者用更小的模型如果是解析慢检查正则是否有回溯爆炸如果是锁竞争考虑把状态存储改成无锁结构或者分片。我自己的项目里最大的瓶颈出在状态同步的锁竞争上。后来把状态按会话ID分片每个分片独立加锁P99延迟从800毫秒降到了120毫秒。这个优化带来的收益比换模型还大。7. 我踩过的坑与实操心得说几个文档里不会写、但实际做项目一定会遇到的坑。第一个坑不要用模型做所有事。早期我试图用模型做意图识别、槽位填充、上下文压缩、响应校验结果成本和延迟都失控。后来把能规则化的全部规则化模型只负责真正需要语义理解的部分整体成本降了60%以上。第二个坑置信度阈值要动态调整。固定阈值在不同场景下表现差异很大。后来改成根据历史准确率动态调整某类意图的历史准确率高阈值就降低让更多请求走自动流程准确率低阈值就提高让更多请求走澄清或人工。这个动态机制上线后整体自动处理率提升了12个百分点。第三个坑日志要记全但不要记敏感信息。排查问题需要完整的输入输出日志但用户输入里可能包含个人信息。我的做法是日志分级调试级别记全量但只保留24小时信息级别记脱敏后的摘要保留30天。这样既能排查问题又不会带来合规风险。第四个坑不要追求一次做完美。Optimiser是一个需要持续迭代的组件。我的建议是先用最简单的规则跑起来收集真实数据然后根据数据逐步优化。一上来就设计复杂的多级流水线大概率是过度设计而且没有数据支撑的优化都是瞎猜。最后分享一个实用的小技巧给Optimiser加一个“影子模式”。新策略上线时先不真正生效而是把新旧策略的结果都算出来对比差异。跑一周确认新策略确实更好之后再正式切换。这个模式帮我避免了好几次因为策略调整导致的线上事故。
阅读完成 · 觉得有帮助?