1. 演示惊艳上线稀碎,问题到底出在哪一层
做Agent项目的人,大概都经历过这种落差:内部演示时,任务规划丝滑、工具调用精准、多轮对话上下文稳如老狗,会议室里一片“这个方向对了”的赞叹;真到了线上,用户一句稍微绕一点的提问,Agent就开始胡言乱语,工具调用参数错得离谱,多轮对话到第三轮就忘了自己是谁。演示环境里那个“聪明助手”,上线后变成了一个“随机行为发生器”。
我前后参与过几个Agent类项目的落地,从早期的单轮工具调用,到后来的多步规划加记忆系统,踩过的坑基本能画出一张完整的“演示到上线”的鸿沟地图。这张地图上,模型能力只占一小块,剩下的大片区域,全是工程问题。标题里说“把锅归给工程”,不是甩锅,是实话。模型是发动机,工程是底盘、传动、散热和刹车。发动机再好,底盘散架,车也上不了路。
这篇文章想聊的,就是Agent项目从演示到上线之间,那些工程层面的断层到底长什么样,以及怎么一步步把它们填上。适合正在做Agent落地、被线上效果折磨过的开发者,也适合刚接触Agent、想提前避开这些坑的团队。我会尽量把每个问题的根因、排查思路和修复方案讲透,不堆术语,不绕弯子。
先给一个整体判断:Agent项目的工程复杂度,被严重低估了。大家习惯用“模型能力”来解释一切,但线上效果差,十有八九是工程链路里某个环节在悄悄漏气。下面我从几个最要命的断层开始拆。
2. 演示环境与线上环境的五个工程断层
2.1 输入分布的断层:演示时你喂的是“标准题”,线上用户给的是“野生题”
演示时,我们习惯用精心设计的prompt,问题清晰、意图明确、上下文完整。比如“帮我查一下明天北京的天气,然后推荐一个适合户外活动的时段”。这种输入,模型很容易解析出两个子任务:查天气、推荐时段。工具调用参数也规整:城市=北京,日期=明天。
线上用户不会这么配合。他们可能说“明天想出去走走,北京这天气行不行”,或者“帮我看看明天适不适合跑步,我在北京”。更麻烦的是,用户会带错别字、口语省略、多意图混杂、甚至情绪化表达。比如“明天北京天气咋样啊,烦死了最近老下雨”。这句话里,“烦死了最近老下雨”是情绪表达,不是任务指令,但模型可能把它当成一个需要处理的意图,导致工具调用跑偏。
这个断层的本质是:演示时的输入分布是窄的、干净的,线上输入分布是宽的、脏的。模型在窄分布上表现好,不代表在宽分布上稳。工程上要做的,不是指望模型自己泛化,而是在输入进入模型之前,加一层“输入规范化”处理。
具体怎么做?我常用的方案是:在Agent的入口处加一个轻量级的预处理模块,负责意图澄清和输入清洗。这个模块不需要太复杂,可以用规则加小模型的方式。规则负责处理明显的噪声,比如去除语气词、纠正常见错别字、识别并剥离情绪表达;小模型负责判断输入是否包含多个意图,如果包含,就拆成多个子任务,分别送入Agent处理。
这里有个经验:不要试图用一个prompt解决所有输入问题。演示时那种“一个prompt打天下”的做法,线上必死。输入预处理要独立成一个工程环节,有自己的测试用例和评估指标。我一般会收集线上真实用户输入,人工标注出“意图清晰度”和“任务复杂度”,然后看预处理模块在这些样本上的表现。如果预处理后意图清晰度提升不明显,说明规则或模型需要调整。
还有一个容易被忽略的点:输入的长度分布。演示时输入通常很短,线上用户可能粘贴一大段文本,或者多轮对话累积了很长的上下文。模型对长输入的处理能力会下降,尤其是当输入里包含大量无关信息时。工程上要做输入截断或摘要,但截断策略很讲究。简单截断可能丢掉关键信息,我倾向于用“关键信息抽取”的方式,先把输入里的实体、时间、地点、动作抽出来,再拼成精简的prompt送给模型。
2.2 工具调用的断层:演示时工具是“理想接口”,线上工具是“脾气各异的黑盒”
演示时,我们调用的工具往往是精心包装过的,输入输出格式规整,响应时间稳定,错误率极低。比如一个查询天气的API,演示时永远返回结构化的JSON,字段齐全,没有异常。线上呢?API可能超时、返回格式变了、字段缺失、甚至返回一个HTML错误页。更常见的是,工具的参数要求比演示时复杂得多。演示时可能只需要传城市名,线上可能需要传城市编码、日期格式、语言偏好、单位制等一堆参数。
这个断层的核心是:演示时工具是“理想接口”,线上工具是“脾气各异的黑盒”。Agent在演示时学会了调用这个工具,但线上工具的行为变了,Agent就懵了。
工程上要做的,是在Agent和工具之间加一层“工具适配层”。这层适配层负责:参数校验与补全、响应解析与容错、超时与重试、降级策略。
参数校验与补全:Agent生成的工具调用参数,不能直接透传给工具。要先经过校验,看必填参数是否齐全、格式是否正确。如果缺失,适配层可以根据上下文补全,或者返回一个澄清请求给Agent,让它重新生成。比如Agent调用天气工具时只传了城市,适配层发现日期参数缺失,可以从对话上下文中提取“明天”并转换成具体日期。
响应解析与容错:工具返回的结果,不能直接塞回给Agent。要先解析,看是否符合预期格式。如果格式不对,适配层要尝试修复,或者返回一个标准化的错误信息给Agent。比如工具返回了HTML错误页,适配层要识别出来,转成“工具暂时不可用”的结构化消息,而不是把HTML原文丢给模型,让模型去猜。
超时与重试:线上工具响应时间不稳定,必须有超时机制。超时后是重试还是降级,要根据业务场景决定。查询类工具可以重试,写入类工具要谨慎。重试次数和间隔也要设置合理,避免雪崩。
降级策略:当工具不可用时,Agent不能直接挂掉。适配层要提供降级方案,比如返回缓存数据、返回默认值、或者引导用户稍后再试。降级策略要提前设计好,不能等线上出问题了再临时想。
我踩过的一个坑是:演示时工具调用成功率100%,线上第一次跑就遇到工具返回格式变化,Agent直接把错误信息当成工具结果,继续往下执行,最后输出一个完全错误的答案。后来加了适配层,工具返回的任何异常都会被拦截,转成标准错误消息,Agent收到后知道要重试或告知用户。这个改动之后,工具调用相关的线上故障率下降了一个数量级。
2.3 上下文管理的断层:演示时上下文是“短而干净”,线上上下文是“长而混乱”
演示时,多轮对话通常只有三五轮,上下文短,信息密度高,模型很容易记住关键信息。线上呢?用户可能聊了二十轮,中间穿插了多个话题,上下文里充满了无关信息、重复信息、甚至矛盾信息。模型在长上下文里找关键信息的能力会急剧下降,表现为“忘了之前说过什么”“把不同轮次的信息搞混”“重复问已经回答过的问题”。
这个断层的本质是:演示时的上下文管理是“隐式”的,靠模型自己的注意力机制;线上需要“显式”的上下文管理,靠工程手段来维护一个干净、结构化的上下文。
工程上要做的,是引入“上下文管理器”。这个管理器负责:上下文压缩、关键信息抽取、上下文分段、以及上下文检索。
上下文压缩:当对话轮次超过一定数量,或者上下文长度超过阈值,就要压缩。压缩不是简单截断,而是把历史对话总结成摘要。摘要要保留关键实体、意图、已确认的信息、未完成的任务。我一般用一个小模型来做摘要,prompt里明确要求保留哪些信息。摘要后的上下文长度控制在模型窗口的30%到50%,留出空间给当前轮次的输入。
关键信息抽取:除了摘要,还要维护一个“会话状态”对象,结构化地存储关键信息。比如用户ID、当前任务、已收集的参数、已完成步骤、待确认事项。这个状态对象不依赖模型记忆,而是由工程代码维护。每次Agent处理完一轮,就更新这个状态对象。下一轮开始时,把状态对象和摘要一起拼进prompt。这样即使模型忘了,工程层还记得。
上下文分段:长上下文里,不同轮次的信息重要性不同。最近的轮次通常更重要,早期的轮次可能只是背景。工程上可以把上下文分成“近期上下文”和“远期摘要”,近期上下文保留原文,远期摘要保留要点。模型处理时,近期上下文权重更高。
上下文检索:当上下文特别长,或者需要跨会话记忆时,可以用向量检索的方式,从历史对话中检索相关片段。但检索的精度很关键,检索错了会引入噪声。我一般只在特定场景用检索,比如用户问“我之前说的那个事”,需要从历史里找。检索结果要经过相关性过滤,只保留高置信度的片段。
这里有个经验:不要指望模型自己管理上下文。模型在长上下文里的表现,远不如工程层显式管理来得稳。我见过太多项目,演示时上下文短,模型表现好,上线后上下文一长,模型就开始胡言乱语。加了上下文管理器之后,多轮对话的连贯性明显提升。
2.4 状态一致性的断层:演示时状态是“单线程”,线上状态是“并发多线程”
演示时,通常只有一个用户,一个会话,状态简单。线上呢?多个用户并发,每个用户可能有多个会话,每个会话可能有多个任务并行。状态管理变得极其复杂。常见的问题包括:用户A的操作影响了用户B的状态、同一个用户的两个会话互相干扰、任务执行到一半状态丢失、重试导致状态重复更新。
这个断层的本质是:演示时的状态管理是“单线程”的,线上是“并发多线程”的。工程上要做的,是引入“状态存储与隔离”机制。
状态存储:Agent的会话状态不能放在内存里,要持久化到外部存储。可以用Redis、数据库、或者对象存储。存储的粒度要细,每个会话一个状态记录,每个任务一个状态记录。状态更新要原子化,避免并发写冲突。
状态隔离:不同用户、不同会话、不同任务的状态要严格隔离。用命名空间或者租户ID来区分。我一般用“用户ID+会话ID+任务ID”作为状态键,确保唯一性。状态读取和写入都要带这个键,避免串号。
状态版本控制:并发场景下,状态可能被多个请求同时修改。要用乐观锁或悲观锁来保证一致性。乐观锁适合读多写少的场景,悲观锁适合写多的场景。我一般用版本号加CAS操作,每次更新状态时检查版本号,如果版本号变了,说明有并发修改,需要重试或合并。
状态恢复:任务执行到一半,如果进程挂了或者超时了,状态不能丢。要有状态恢复机制,从持久化存储里读取最新状态,继续执行。恢复时要判断任务是否已经完成,避免重复执行。我一般给每个任务加一个“执行状态”字段,比如pending、running、completed、failed。恢复时只处理running状态的任务。
踩过的一个坑是:演示时状态放在内存里,上线后多用户并发,状态互相覆盖,导致用户A的查询结果返回给了用户B。后来把状态移到Redis,加了用户隔离,问题解决。这个坑的教训是:状态管理不能图省事,演示时的单线程思维,线上会死得很惨。
2.5 错误处理的断层:演示时错误是“异常”,线上错误是“常态”
演示时,我们假设一切顺利,错误处理往往很粗糙,甚至没有。线上呢?错误是常态。工具超时、模型返回格式错误、网络抖动、用户输入非法、并发冲突,各种错误层出不穷。如果没有完善的错误处理机制,Agent会频繁崩溃,或者输出不可预期的结果。
这个断层的本质是:演示时的错误处理是“事后补救”,线上需要“事前设计”。工程上要做的,是建立一套完整的错误处理框架。
错误分类:先把错误分类。我一般分成四类:输入错误、工具错误、模型错误、系统错误。输入错误是用户输入不合法或无法理解;工具错误是工具调用失败或返回异常;模型错误是模型输出格式不对或内容不合规;系统错误是网络、存储、并发等基础设施问题。不同类别的错误,处理策略不同。
错误传播:错误不能直接抛给用户,也不能直接丢给模型。要有中间层来拦截和转换。输入错误要返回澄清请求,引导用户重新输入;工具错误要重试或降级;模型错误要重新生成或修正;系统错误要记录日志并返回友好提示。
错误恢复:对于可恢复的错误,要有自动恢复机制。比如工具超时,可以重试;模型输出格式错误,可以重新生成;并发冲突,可以重试。重试要有次数限制和退避策略,避免无限重试。
错误监控:线上错误要实时监控,及时发现和处理。我一般会记录每个错误的类型、发生时间、上下文、处理结果。然后设置告警阈值,比如某个错误率超过5%就告警。监控数据还可以用来分析错误模式,找出高频错误,针对性优化。
这里有个经验:错误处理不是加个try-catch就完事了。要设计一套完整的错误处理流程,从错误发生、分类、传播、恢复到监控,每个环节都要有明确的策略。我见过太多项目,演示时跑得通,上线后一遇到错误就崩,就是因为错误处理没设计好。
3. 从演示到上线,工程侧要补的六件事
3.1 建立输入预处理管道,把“野生输入”变成“可处理输入”
输入预处理管道是Agent上线的第一道工程防线。它的目标不是让输入变得完美,而是让输入变得“可处理”。具体来说,要做三件事:清洗、规范化、意图澄清。
清洗:去除输入中的噪声,比如多余空格、特殊字符、语气词、情绪表达。但清洗要谨慎,不能把有用信息也洗掉。我一般用规则加小模型的方式,规则处理明显噪声,小模型判断哪些内容是噪声。比如“明天北京天气咋样啊,烦死了最近老下雨”,规则可以去掉“啊”,小模型可以识别“烦死了最近老下雨”是情绪表达,可以剥离或标记为情绪,不影响任务解析。
规范化:把输入转换成模型更容易理解的格式。比如把口语化的表达转成结构化表达,把相对时间转成绝对时间,把省略的主语补全。我一般会定义一个“规范化模板”,把常见表达映射到标准格式。比如“明天”映射到具体日期,“北京”映射到城市编码。规范化后的输入,模型解析起来更稳。
意图澄清:判断输入是否包含多个意图,如果包含,就拆成多个子任务。拆分的粒度要适中,太粗了模型处理不了,太细了增加调用次数。我一般按“动作+对象”来拆,比如“查天气”和“推荐时段”是两个动作,拆成两个子任务。拆分后,每个子任务单独送入Agent处理,最后合并结果。
输入预处理管道要有自己的测试集和评估指标。我一般会收集线上真实输入,人工标注“意图清晰度”和“任务复杂度”,然后看预处理后的输入在这些指标上的提升。如果提升不明显,说明管道需要调整。
3.2 工具适配层:让Agent面对的工具接口“永远稳定”
工具适配层的核心目标是:不管底层工具怎么变,Agent看到的接口永远稳定。这层适配层要处理四件事:参数校验、响应解析、超时重试、降级策略。
参数校验:Agent生成的工具调用参数,先经过校验。校验规则包括:必填参数是否齐全、参数格式是否正确、参数值是否在允许范围内。如果校验失败,适配层可以尝试补全,或者返回错误给Agent,让它重新生成。补全的逻辑要基于上下文,比如从对话历史里提取缺失的参数。
响应解析:工具返回的结果,先经过解析。解析规则包括:响应格式是否符合预期、关键字段是否存在、字段类型是否正确。如果解析失败,适配层要尝试修复,或者返回标准错误。修复的逻辑要保守,不能瞎猜。比如工具返回了HTML错误页,适配层要识别出来,转成“工具暂时不可用”的结构化消息。
超时重试:每个工具调用都要设置超时时间。超时后,根据工具类型决定是否重试。查询类工具可以重试,写入类工具要谨慎。重试次数和间隔要设置合理,我一般设置最多重试2次,间隔1秒和3秒。重试时要记录日志,方便排查。
降级策略:当工具不可用时,要有降级方案。降级方案包括:返回缓存数据、返回默认值、引导用户稍后再试。降级策略要提前设计好,不能等线上出问题了再临时想。我一般会给每个工具定义一个降级响应,当工具调用失败时,返回降级响应,Agent收到后知道要告知用户或走备用流程。
工具适配层要独立于Agent逻辑,单独测试和维护。我一般会为每个工具写单元测试,模拟各种异常情况,确保适配层能正确处理。
3.3 上下文管理器:让模型在长对话里“不忘事、不串台”
上下文管理器的核心目标是:让模型在长对话里保持连贯,不忘记关键信息,不混淆不同轮次的内容。它要处理四件事:上下文压缩、状态维护、上下文分段、上下文检索。
上下文压缩:当对话轮次超过阈值,或者上下文长度超过限制,就压缩。压缩的方式是摘要,把历史对话总结成要点。摘要要保留关键实体、意图、已确认信息、未完成任务。我一般用一个小模型来做摘要,prompt里明确要求保留哪些信息。摘要后的上下文长度控制在模型窗口的30%到50%。
状态维护:维护一个结构化的会话状态对象,存储关键信息。状态对象包括:用户ID、当前任务、已收集参数、已完成步骤、待确认事项。状态对象由工程代码维护,不依赖模型记忆。每次Agent处理完一轮,就更新状态对象。下一轮开始时,把状态对象和摘要一起拼进prompt。
上下文分段:把上下文分成“近期上下文”和“远期摘要”。近期上下文保留原文,远期摘要保留要点。模型处理时,近期上下文权重更高。分段的阈值根据模型窗口和任务复杂度来定,我一般把最近5轮作为近期上下文,更早的作为远期摘要。
上下文检索:当需要跨会话记忆,或者从历史里找特定信息时,用向量检索。检索的精度很关键,检索错了会引入噪声。我一般只在特定场景用检索,比如用户问“我之前说的那个事”。检索结果要经过相关性过滤,只保留高置信度的片段。
上下文管理器要定期评估效果。我一般会设计一些测试用例,比如“多轮对话后是否还记得用户偏好”“是否会把不同轮次的信息搞混”。如果效果不好,就调整压缩策略、状态字段、分段阈值或检索参数。
3.4 状态存储与隔离:并发场景下不串号、不丢状态
状态存储与隔离的核心目标是:在并发场景下,每个用户、每个会话、每个任务的状态都独立、持久、可恢复。它要处理四件事:状态持久化、状态隔离、状态版本控制、状态恢复。
状态持久化:状态不能放在内存里,要持久化到外部存储。可以用Redis、数据库、或者对象存储。存储的粒度要细,每个会话一个状态记录,每个任务一个状态记录。状态更新要原子化,避免并发写冲突。
状态隔离:不同用户、不同会话、不同任务的状态要严格隔离。用命名空间或者租户ID来区分。我一般用“用户ID+会话ID+任务ID”作为状态键,确保唯一性。状态读取和写入都要带这个键,避免串号。
状态版本控制:并发场景下,状态可能被多个请求同时修改。要用乐观锁或悲观锁来保证一致性。乐观锁适合读多写少的场景,悲观锁适合写多的场景。我一般用版本号加CAS操作,每次更新状态时检查版本号,如果版本号变了,说明有并发修改,需要重试或合并。
状态恢复:任务执行到一半,如果进程挂了或者超时了,状态不能丢。要有状态恢复机制,从持久化存储里读取最新状态,继续执行。恢复时要判断任务是否已经完成,避免重复执行。我一般给每个任务加一个“执行状态”字段,比如pending、running、completed、failed。恢复时只处理running状态的任务。
状态存储与隔离要定期做压力测试。我一般会模拟多用户并发,看状态是否串号、是否丢失、是否一致。如果发现问题,就调整存储方案、隔离策略或版本控制机制。
3.5 错误处理框架:把“异常”变成“可预期流程”
错误处理框架的核心目标是:把线上各种异常,变成可预期的处理流程,让Agent在遇到错误时,能优雅地降级或恢复,而不是崩溃或输出错误结果。它要处理四件事:错误分类、错误传播、错误恢复、错误监控。
错误分类:先把错误分类。我一般分成四类:输入错误、工具错误、模型错误、系统错误。输入错误是用户输入不合法或无法理解;工具错误是工具调用失败或返回异常;模型错误是模型输出格式不对或内容不合规;系统错误是网络、存储、并发等基础设施问题。不同类别的错误,处理策略不同。
错误传播:错误不能直接抛给用户,也不能直接丢给模型。要有中间层来拦截和转换。输入错误要返回澄清请求,引导用户重新输入;工具错误要重试或降级;模型错误要重新生成或修正;系统错误要记录日志并返回友好提示。
错误恢复:对于可恢复的错误,要有自动恢复机制。比如工具超时,可以重试;模型输出格式错误,可以重新生成;并发冲突,可以重试。重试要有次数限制和退避策略,避免无限重试。
错误监控:线上错误要实时监控,及时发现和处理。我一般会记录每个错误的类型、发生时间、上下文、处理结果。然后设置告警阈值,比如某个错误率超过5%就告警。监控数据还可以用来分析错误模式,找出高频错误,针对性优化。
错误处理框架要覆盖所有可能的错误路径。我一般会做故障注入测试,模拟各种错误,看框架是否能正确处理。如果发现未处理的错误路径,就补上。
3.6 可观测性建设:让线上问题“可定位、可复现、可修复”
可观测性建设的核心目标是:当线上出问题时,能快速定位原因、复现问题、修复问题。它要处理三件事:日志、指标、追踪。
日志:记录Agent处理过程中的关键事件,包括输入、输出、工具调用、状态变更、错误。日志要结构化,方便查询和分析。我一般用JSON格式,包含时间戳、用户ID、会话ID、任务ID、事件类型、事件内容。日志级别要合理,避免过多或过少。
指标:记录Agent的关键指标,包括请求量、成功率、延迟、错误率、工具调用次数、模型调用次数。指标要实时采集和展示,方便监控趋势。我一般用Prometheus加Grafana,设置告警规则。
追踪:记录一个请求在Agent内部的完整调用链路,包括输入预处理、模型调用、工具调用、状态更新、输出生成。追踪要能跨服务,方便定位瓶颈。我一般用OpenTelemetry,把每个环节的耗时和结果记录下来。
可观测性建设要覆盖Agent的所有关键路径。我一般会定期做“可观测性演练”,模拟线上问题,看是否能通过日志、指标、追踪快速定位。如果定位不了,就补充可观测性数据。
4. 一个模拟项目的工程改造实录
4.1 改造前的状态:演示满分,上线崩盘
假设有一个模拟项目,是一个“智能日程助手”Agent,能帮用户查天气、推荐活动、安排日程。演示时,用预设的输入,Agent表现完美:解析意图准确、工具调用正确、多轮对话连贯。上线后,问题频出:用户输入稍微绕一点就解析错、工具调用经常失败、多轮对话到第三轮就忘事、并发时状态串号、错误处理几乎没有。
这个模拟项目的技术栈是:模型用某个通用大模型,工具调用用function calling,状态放在内存里,错误处理只有简单的try-catch。演示时,这些都没问题,因为输入干净、工具稳定、单用户、无错误。上线后,输入变脏、工具变不稳定、多用户并发、错误频发,整个系统就崩了。
4.2 改造步骤一:加输入预处理,把脏输入洗干净
第一步是加输入预处理管道。我们收集了线上真实用户输入,人工标注了“意图清晰度”和“任务复杂度”,然后设计预处理规则和模型。
规则部分:去除多余空格、特殊字符、语气词。比如“明天北京天气咋样啊”变成“明天北京天气咋样”。模型部分:用一个小模型判断输入是否包含多个意图,如果包含,就拆成多个子任务。比如“明天想出去走走,北京这天气行不行”拆成“查明天北京天气”和“推荐适合户外活动的时段”。
预处理后,输入进入Agent之前,先经过这个管道。效果很明显:意图解析准确率从演示时的95%提升到线上的90%以上,虽然比演示时低,但比改造前的70%好太多。
4.3 改造步骤二:加工具适配层,让工具调用不再“看天吃饭”
第二步是加工具适配层。我们为每个工具定义了参数校验规则、响应解析规则、超时重试策略、降级方案。
参数校验:Agent生成的工具调用参数,先经过校验。比如查天气工具,必填参数是城市和日期。如果Agent只传了城市,适配层从上下文里提取日期补全。如果提取不到,返回错误给Agent,让它重新生成。
响应解析:工具返回的结果,先经过解析。比如天气工具返回JSON,适配层检查关键字段是否存在。如果不存在,返回标准错误。如果工具返回HTML错误页,适配层识别出来,转成“工具暂时不可用”的结构化消息。
超时重试:每个工具调用设置超时时间,比如3秒。超时后重试最多2次,间隔1秒和3秒。重试时记录日志。
降级策略:当工具不可用时,返回缓存数据或默认值。比如天气工具不可用,返回“天气信息暂时不可用,请稍后再试”。
改造后,工具调用成功率从线上的60%提升到95%以上,工具相关的错误率下降了一个数量级。
4.4 改造步骤三:加上下文管理器,让多轮对话不再“失忆”
第三步是加上下文管理器。我们引入了上下文压缩、状态维护、上下文分段、上下文检索。
上下文压缩:当对话轮次超过5轮,或者上下文长度超过模型窗口的50%,就压缩。压缩用一个小模型做摘要,保留关键实体、意图、已确认信息、未完成任务。
状态维护:维护一个结构化的会话状态对象,存储用户ID、当前任务、已收集参数、已完成步骤、待确认事项。每次Agent处理完一轮,就更新状态对象。下一轮开始时,把状态对象和摘要一起拼进prompt。
上下文分段:最近5轮作为近期上下文,保留原文;更早的作为远期摘要,保留要点。
上下文检索:当用户问“我之前说的那个事”时,用向量检索从历史里找相关片段。检索结果经过相关性过滤,只保留高置信度的片段。
改造后,多轮对话的连贯性明显提升。用户反馈“终于记得我说过什么了”。
4.5 改造步骤四:加状态存储与隔离,让并发不再“串号”
第四步是加状态存储与隔离。我们把状态从内存移到Redis,用“用户ID+会话ID+任务ID”作为状态键,确保唯一性。状态更新用版本号加CAS操作,保证一致性。任务执行状态用pending、running、completed、failed标记,支持状态恢复。
改造后,并发场景下状态不再串号,任务执行到一半挂了也能恢复。压力测试显示,100个并发用户下,状态一致性100%。
4.6 改造步骤五:加错误处理框架,让异常不再“崩盘”
第五步是加错误处理框架。我们把错误分成四类:输入错误、工具错误、模型错误、系统错误。每类错误有对应的处理策略。输入错误返回澄清请求;工具错误重试或降级;模型错误重新生成或修正;系统错误记录日志并返回友好提示。
错误恢复:可恢复的错误自动恢复,重试有次数限制和退避策略。
错误监控:记录每个错误的类型、时间、上下文、处理结果,设置告警阈值。
改造后,线上错误率从改造前的15%降到2%以下,且大部分错误能自动恢复。
4.7 改造步骤六:加可观测性,让问题“可定位、可复现、可修复”
第六步是加可观测性。我们加了日志、指标、追踪。
日志:结构化JSON日志,记录输入、输出、工具调用、状态变更、错误。
指标:请求量、成功率、延迟、错误率、工具调用次数、模型调用次数,用Prometheus加Grafana展示。
追踪:用OpenTelemetry记录完整调用链路,包括输入预处理、模型调用、工具调用、状态更新、输出生成。
改造后,线上问题定位时间从小时级降到分钟级。有一次用户反馈“Agent回答错了”,我们通过追踪快速定位到是工具返回格式变化导致解析失败,修复后上线,问题解决。
5. 几个容易踩的工程坑和我的处理习惯
5.1 不要用演示的prompt直接上线
演示时的prompt,往往是为了展示效果精心调过的,可能包含很多“暗示”或“引导”。比如prompt里写了“请仔细分析用户意图,如果包含多个任务,请拆分成子任务”。这种prompt在演示时有效,因为输入干净,模型容易遵循。线上输入脏了,模型可能忽略这些指令,或者过度拆分。
我的习惯是:演示用的prompt和线上用的prompt分开维护。线上prompt要更简洁、更鲁棒,不依赖模型的“自觉性”,而是靠工程层来保证。比如意图拆分,不靠prompt让模型拆,而是靠输入预处理管道来拆。
5.2 工具调用不要依赖模型的“自由发挥”
演示时,模型可能“自由发挥”调用工具,参数也生成得不错。线上,模型的自由发挥往往不靠谱。我的习惯是:工具调用的参数,尽量用结构化模板来约束。比如定义一个JSON schema,要求模型按schema生成参数。如果模型生成的参数不符合schema,适配层拦截并重新生成。
5.3 上下文不是越长越好
演示时,上下文短,模型表现好。线上,有人觉得上下文越长越好,把历史对话全塞进去。实际上,上下文越长,模型越容易分心,关键信息越容易被淹没。我的习惯是:上下文要压缩,保留关键信息,去掉冗余。摘要加状态对象的方式,比全量上下文更稳。
5.4 状态管理不要图省事
演示时,状态放内存里,简单。线上,状态放内存里,多用户并发就串号。我的习惯是:状态从一开始就持久化,用外部存储,加隔离和版本控制。虽然前期麻烦点,但上线后省心。
5.5 错误处理不要等上线后再补
演示时,错误处理往往被忽略。线上,错误是常态。我的习惯是:错误处理框架在开发阶段就设计好,覆盖所有可能的错误路径。做故障注入测试,模拟各种错误,看框架是否能正确处理。
5.6 可观测性不要等出问题再建
演示时,可观测性不重要。线上,出问题时没有可观测性,定位问题像大海捞针。我的习惯是:可观测性在开发阶段就建好,日志、指标、追踪都加上。上线后,问题定位快,修复快。
6. 上线之后,Agent工程还能往哪走
Agent上线不是终点,而是起点。上线后,会收到真实用户反馈,会发现新的问题,会有新的需求。工程上要继续迭代。
一个方向是“自适应”:根据线上数据,自动调整输入预处理规则、工具适配策略、上下文压缩参数。比如发现某类输入经常解析错,就自动加规则或调整模型。发现某个工具经常超时,就自动调整超时时间或降级策略。
另一个方向是“可解释”:让Agent的决策过程可解释,方便排查问题和优化。比如记录Agent为什么选择这个工具、为什么生成这个参数、为什么这样回复。可解释性可以帮助定位问题,也可以帮助优化prompt和工程逻辑。
还有一个方向是“多Agent协作”:单个Agent能力有限,多个Agent协作可以处理更复杂的任务。但多Agent协作的工程复杂度更高,需要解决通信、协调、状态同步等问题。这是后续可以探索的方向。
我个人在实际操作中的体会是:Agent项目的工程化,是一个持续迭代的过程。演示到上线的鸿沟,不是一次性能填平的,而是通过一次次线上问题、一次次工程改造,慢慢缩小的。关键是,不要把问题都归给模型,要看到工程侧的机会。工程做扎实了,模型的能力才能发挥出来。