去年我参与推进了一个企业级知识库 Agent 项目,整个过程让我印象极深。Demo 演示的时候,业务副总裁当场拍板“下个月就上”,会议室里一片叫好;结果灰度两周,差评如潮——答非所问、重复下单、超时转圈、权限混乱,最后只能临时撤下。复盘时我们达成一个共识:Agent 在 Demo 里惊艳,是因为 Demo 展示的是模型的上限;上线拉胯,是因为生产环境检验的是系统的下限。
这篇文章就是围绕这个落差展开的。我会先拆解“Demo 惊艳、上线拉胯”的根因到底是什么,再讲清楚我在实际项目中反复踩过的四道坎——并发、工具调用确定性、记忆管理、可观测与安全治理——以及对应的工程解法。内容不面向算法调参,而是面向那些真正要“把 Agent 跑在业务系统里”的工程负责人。不管你是用 LangGraph、Spring AI 还是自研编排,下面这些坑大概率都能对应上。
1. 根因拆解:Demo 只证明了“模型能做”,生产要求的是“系统可靠”
1.1 演示环境从未触碰的那些“生产约束”
先说一个很容易被忽略的事实:Demo 环境里,Agent 的所有外部条件几乎都是被“保护”起来的。演示台上只有一两个用户,提问是提前筛选过的,网络走的是内网或者高质量链路,工具调用也刻意挑了成功率最高的那几个。甚至最关键的兜底——一旦 Agent 答偏了,旁边会有人解释或手工补一刀。
生产环境完全不是这样。真实用户输入的 prompt 五花八门,同一个问题换十种说法;工具调用涉及的子系统可能时好时坏,第三方接口超时是家常便饭;系统要 7x24 小时跑着,没人盯着随时准备兜底;更重要的是,生产系统同时在线几百上千人,每个人有自己的上下文、权限、业务数据、历史会话。
我做了张对比表,每次评审会上我都会直接贴出来:
| 维度 | Demo 环境 | 生产环境 |
|---|---|---|
| 并发用户 | 1~2 个 | 数百到数千,峰值难以预估 |
| 输入质量 | 精心准备的测试问题 | 真实业务噪声、错别字、口语化 |
| 工具成功率 | 只演示已验证的高概率路径 | 全量已接入工具,失败是常态 |
| 兜底方式 | 人工解释、现场重试 | 无人工实时兜底,只能靠系统自愈 |
| 运行时长 | 几分钟 | 7x24 小时,跨周跨月 |
| 状态管理 | 单轮上下文 | 多轮、多会话、跨时段长期记忆 |
| 权限边界 | 临时放开的调试账号 | 多租户、多角色、严格数据隔离 |
| 可观测性 | 几乎不关心内部过程 | 必须可追踪、可审计、可告警 |
这张表的结论也很直白:Demo 的评分维度是“模型有没有能力”,生产环境的评分维度是“系统稳不稳定、边界清不清晰、出了事能不能处理”。这是两套评价体系,只把 Demo 当验收标准,上线就注定要出问题。
1.2 根因不是模型变笨,而是围绕模型的工程系统没建起来
很多人一遇到上线效果下降,第一反应是“模型没选好”“prompt 没写对”。但我复盘过多次之后发现,绝大多数上线拉胯根本不是模型智力下降,而是工程能力缺位。
举几个高频现场:并发一上来,Agent 进程因状态装在内存里导致会话错乱;某个下游接口偶发 500,Agent 重试三遍之后给你重复建了三笔工单;用户聊到第 40 轮,上下文窗口爆了,Agent 把最早的业务约束“忘了”;还有的 Agent 犯错了,你根本无法定位它到底调了哪个工具,输出了什么,为什么选这一步。
这些问题的共同点是:在 Demo 里没人测,或者测了也看不出来,因为 Demo 总是“恰好走通”。工程化的本质,就是把“恰好走通”变成“大部分情况下稳定走通,少数失败时有明确的处理路径”。接下来我要拆的四道坎,就是在补“稳定走通”和“失败处理路径”这两件事。
2. 第一道坎:扛并发不是加机器,而是让 Agent 先“无状态化”
2.1 有状态的 Agent 为什么无法水平扩展
很多团队第一版 Agent 是直接把对话历史存在进程内的内存变量里,比如一个 Python dict,key 是 session_id。单机单用户跑起来毫无问题,但一旦要接生产流量,立刻撞墙:每个请求都必须路由到持有该 session 的那台机器,这叫“粘性会话”。请求稍微一多,你没法把流量打散到多台机器,因为会话状态不共享。
更麻烦的是,多数 LLM 接口调用是秒级甚至是十几秒级响应。如果 Agent 运行逻辑是串行的——一个请求进来,同步等待 LLM 返回,再等工具返回——那单进程的吞吐上限会非常难看。生产里常说的“AI Agent 怎么扛并发”,本质上不是让模型更快,而是把 Agent 的编排过程变成异步、无状态、可横向扩展的管道。
我在项目里定的原则是:Agent 核心逻辑里不允许保存任何“会话私有的可变状态”。所有需要持久化的东西——历史消息、中间步骤结果、用户上下文——一律外置到状态存储。
2.2 会话状态外置:用 Redis 或 Postgres 存中间状态
这里说的状态,不只是“消息历史”。Agent 执行一个复杂任务时,往往会有多个步骤:拆解问题、查数据、调工具、汇总答案。每一步的中间结果都可能需要在下一步使用,甚至在进程崩溃后还能恢复。
我自己常用的方案是:
- Redis 存短期热数据,比如最近 N 轮消息、当前执行步骤指针,TTL 设成几小时到一天。
- Postgres 存结构性会话数据,比如会话归属、用户身份、长期记忆、重要的业务记录。
- 对象存储存大体积附件或检索到的文档片段,只在必要时加载。
运行时本身只做三件事:从存储读入当前会话状态、调用模型编排下一步、把结果写回存储。这样一来,任何一个无状态 Worker 都能处理任何会话的请求,水平扩展就是一个加副本的问题。
我贴一段简化过的伪代码,重点看状态读写的位置:
# 简化示例:无状态异步 Agent 运行器 class StateStore: async def get_session(self, session_id: str) -> list[dict]: ... async def append(self, session_id: str, message: dict) -> None: ... class AgentRuntime: def __init__(self, store: StateStore, llm_client): self.store = store self.llm = llm_client async def handle_message(self, session_id: str, user_input: str) -> str: history = await self.store.get_session(session_id) messages = build_messages(history, user_input) response = await self.llm.chat_async(messages) # 关键是异步非阻塞 await self.store.append(session_id, {"role": "user", "content": user_input}) await self.store.append(session_id, {"role": "assistant", "content": response}) return response这里没有哪怕一个本地可变 dict。并发再高,也只是同一段逻辑在多个 Worker 上跑;状态一致性交还给存储层处理。
2.3 异步编排、队列和背压的配合
把 Agent 运行器做成无状态之后,第二步是控制流量入口。我见过不少直接把 Agent HTTP 接口暴露给前端、每个请求同步抢占一个模型调用的做法。生产流量稍微一波动,模型供应商的限流就会触发,然后一堆请求超时,客户端反复重试,雪上加霜。
更可靠的做法是引入任务队列:前端请求只负责创建任务,返回一个任务 ID;后台 Worker 异步消费队列,一旦完成则通过 WebSocket 或轮询把结果推给前端。这种模式有几个直接好处:
- 突发流量被队列削峰,模型接口不会被瞬时打爆。
- 任务可以设置优先级,比如高优业务先处理。
- 消费失败可以进入死信队列,不会丢请求。
- 背压机制天然成立,队列积压就是最直观的负载信号。
我当时用的是 Redis 的列表或者流结构来做轻量队列,复杂度不高,但效果明显。生产上如果团队已有 Kafka 或 RabbitMQ,直接用现成的就行,核心是“Agent 运行时不能阻塞在请求线程里”。
另外,一定要给单用户/单租户设置并发配额。比如每个企业客户同时只能跑 N 个任务,超出则排队或者直接拒绝。没有配额时,一个部门的一波操作就可能吃掉所有模型预算,其他部门全部排队,这在业务上很难交代。
2.4 限流、重试与熔断:别让 Agent 把下游打垮
Agent 和传统接口最大的区别是:一次用户提问可能触发十次八次工具调用。如果每个工具调用都带试错性重试,下游系统会被流量放大到难以承受。
我的做法是给“工具调用”这层单独设限流和熔断,不能只限制“用户请求”这一层。比如:每个任务最多允许调用某个高频工具 20 次;下游接口连续错误率达到 30% 就直接熔断 30 秒;重试必须带指数退避,默认 500ms 起步,最大不超过 30 秒。这些规则的另一个作用是约束 Agent 的“自嗨”——不会让你看到一段 Agent 在循环调用同一个无用接口的日志而无可奈何。
说到底,并发这道坎的解法路径是很清楚的:无状态化、外置状态、队列削峰、限流熔断。Demo 里一个请求调到底,完全暴露不了这些问题;生产里并发一来,缺了哪一环都会立刻现形。
3. 第二道坎:工具调用不能靠运气,幂等与重试才是工程基石
3.1 生产中最可怕的失败模式:Agent 重试导致重复执行
如果说并发是架构问题,那么工具调用的“确定性”就是 Agent 上线的生死线。Demo 里 Agent 调工具,往往是“调十次成功九次”的高质量路径,偶尔失败了人工补一下。生产里下游系统不会惯着你:网络抖动、超时、返回格式不标准、业务校验不通过,各种失败会以极高的频率出现。
但真正可怕的不是失败,而是 Agent 的“重试”导致业务的重复执行。举一个我亲眼见过的案例:Agent 帮用户提交工单,第一次调用超时了,模型判断“可能没提交成功”,于是自动重试了一次。实际上第一次已经入库了,第二次又创建一个新工单,用户最后看到两张几乎一样的工作单。在 Demo 里你永远看不到这个问题,因为演示环境的下游接口是测试桩,压根不会产生真实副作用。
工具调用要生产可用,第一原则就是:凡是产生副作用的操作,必须幂等或者具备幂等保护。
3.2 在工具调用层统一加幂等拦截
幂等设计有一个很成熟的模式:给每一次“任务内工具执行”生成一个唯一请求 ID,存储层记住 ID 对应的执行结果。如果同一任务内再次请求同一个工具调用,直接返回已存储的结果,而不是再次执行。
我一般会在工具调用包装器里做这件事:
async def execute_tool_with_idempotency( tool_name: str, tool_args: dict, request_id: str, idem_store, ): # request_id 来自 Agent 编排层的执行轨迹,同一个工具、同一段逻辑固定不变 key = f"idem:{tool_name}:{request_id}" cached = await idem_store.get(key) if cached is not None: return cached # 直接返回之前执行的结果,避免重复副作用 # 带上超时与熔断策略执行真正的工具 result = await call_tool_with_timeout(tool_name, tool_args, timeout=10) # 执行成功后把结果写入幂等存储,TTL 建议 24h 以上 await idem_store.set(key, result, ttl=60 * 60 * 24) return result这里要注意一个细节:request_id 不能每次重试都重新生成,必须由 Agent 编排层在“第一次尝试该工具”时固定下来。实际落地时,我会在 Agent 的每一步轨迹里都挂一个 step_id,模型尝试调用工具时往上下文里带上这个 step_id,工具执行层统一拿它做幂等键。简单点,也可以用“会话 ID + 意图 hash + 工具名”拼一个稳定键,但稳定性需要长期校验,我更推荐显式 step_id。
除了幂等,还要处理“可安全重复”和“不可重复”两类工具的差异。读操作天然幂等,比如查库存、查订单;写操作要分情况:建单、发消息、转账这类坚决要幂等护栏;状态更新类操作要尽量设计成条件更新,比如“只有该字段处于待处理状态时才能执行”。
3.3 超时、隔离和失败恢复的统一策略
工具调用失败之后,Agent 怎么处理?这个问题必须在工程层给模型一个明确的引导,不能完全靠模型临场发挥。
我维护了一份工具策略清单,每条工具在接入时就要定义:
- 超时时间:默认 3~10 秒,长任务单独配置。
- 重试次数:最多重试 1~2 次,且必须指数退避。
- 失败文案:给 Agent 一句标准的“工具不可用”提示,说明出错类型,避免它瞎猜。
- 审批要求:高风险操作必须走人工审批。
另外,工具执行必须做好隔离。Agent 的回复和下一步决策,不能直接暴露底层异常堆栈。我在生产环境里统一封装为“业务可解释错误”,例如“查询库存服务超时”就足够,不能输出“连接数据库失败:connection refused”这种内部信息。这样既能让模型做出更理性的策略,也避免信息泄露。
3.4 高风险工具必须加人工审批通道
很多人会觉得,Agent 主打自动化,怎么还引入人工审批?这是典型的 Demo 认知。生产环境里,越是高价值的工具,越是要引入“人在环路”的审批机制。电子合同签署、大额支付、群发消息、删除数据,这些工具绝不能允许模型自由触发。
工程化做法是:Agent 发现需要高权限工具之后,先进入“等待审批”状态,生成一条审批任务;审批人通过之后,系统才真正执行工具调用。审批结果会回写到会话状态里,Agent 随后继续它的执行流程。这个机制看起来降低了一点自动化程度,但换来的是一道谁也绕不过的安全边界。
工具调用是 Agent 手脚的延伸。手脚不好使,脑子再好也没用。幂等、超时、隔离、审批,这套组合拳打下来,才敢让 Agent 真正去操作企业核心系统。
4. 第三道坎:记忆管理——把上下文从“窗口”变成“系统”
4.1 上下文窗口不是数据库,会爆,会忘
Agent 在 Demo 阶段通常是“单轮问答”,上下文里塞一段预设背景就够了。到了生产环境,用户会跨多轮、跨天、甚至跨周使用同一个 Agent,它会遇到两个非常具体的问题:
一是上下文窗口溢出。模型输入长度有限,即便当前主流模型支持几十万 token 上下文,真实业务中塞满也只需要几天的高频对话。一旦溢出,有两个处理方向:截断最早的对话或做摘要压缩。截断简单但丢信息,摘要压缩更合理但要做摘要策略。
二是“遗忘”导致业务失真。用户第一轮明确说过“我是华东区的供应链专员”,到了第 20 轮 Agent 可能完全不记得了,回答里开始出现华南区的政策。Demo 环节根本撑不到第 20 轮,所以看不到。
所以记忆这块,必须当做一个持久化系统来设计,而不是依赖模型上下文窗口。
4.2 分层记忆:短期、工作记忆和长期记忆
我在生产项目里把 Agent 记忆拆成三层,分别存储和治理:
| 层级 | 存储内容 | 存储介质 | 生命周期 |
|---|---|---|---|
| 短期记忆 | 当前会话最近几轮原始消息 | Redis,TTL 几小时 | 会话结束即过期 |
| 工作记忆 | 当前任务拆解步骤、中间结果、待办清单 | Redis 或 Postgres | 任务完成即清理 |
| 长期记忆 | 用户偏好、业务约定、历史摘要、关键事实 | Postgres + 向量库 | 数月到数年 |
长期记忆是关键。企业用户会希望 Agent 记住“我上次让查的供应商还没批复”“我更偏好表格形式的报告”“我们的合同编号规则是……”。这些信息如果每次会话都用 prompt 拼进去,成本高而且容易超载。正确做法是把长期记忆单独抽取出来,通过检索动态注入上下文。
4.3 动态记忆检索:只注入“此刻最相关”的内容
一个可落地的实现是双通道记忆检索:
- 结构化通道:从 Postgres 中读取用户标签、业务属性、近期偏好,按照 schema 注入。
- 语义通道:把历史对话分段切片、Embedding 后存入向量库,当前轮提问时做 Top-K 语义检索,召回最相关上下文。
这样既不会把所有历史都塞给模型,也能保证关键信息被找回来。我贴一段简化逻辑:
class MemoryManager: def __init__(self, kv_store, vector_store): self.kv = kv_store self.vectors = vector_store async def load_relevant(self, user_id: str, query: str): # 1. 结构化长期记忆 structured = await self.kv.get(f"profile:{user_id}") # 2. 语义检索历史对话片段 semantic_hits = await self.vectors.search(query, top_k=5) # 3. 合并为上下文 block,交给 Agent 的 prompt 组装层 return merge(structured, semantic_hits)需要注意的是,向量检索在几百条记忆时效果尚可,到了几十万条就非常依赖切片质量与 Embedding 模型的领域适配。不要等到上线后才去做记忆调优,最早一批业务数据出来就要开始验证检索召回率。
4.4 记忆也是数据资产,必须治理
记忆长期化之后,一定会牵扯到数据合规和权限问题。一个用户的信息不能被另一个用户检索到,这是底线。实现上,每条记忆都要带上 owner_id(归属者)和 scope(可见范围),检索时强制带上过滤条件。还要给用户提供“删除我的记忆”的入口,团队内部最好有后台可以查看和清理异常记忆内容。
我的经验是:记忆模块越早设计权限边界越好,否则等记忆量大了再改造,迁移成本巨大。尤其是企业客户,一旦发现自己会话里的敏感信息可能被其他用户检索到,信任崩塌就再也救不回来了。
5. 第四道坎:让 Agent 可观测、可评测、可治理,否则没人敢给你上线
5.1 每个推理步骤都要有追踪记录,包括成本
Agent 与传统服务的最大差异是,它的每次输出背后是一串不可完全预测的模型决策。没有追踪日志,出问题你连复盘都无从下手。所以从第一天起,我就要求 Agent 的每一次执行都必须输出结构化的 Trace 日志,包含如下字段:
| 字段 | 说明 |
|---|---|
| trace_id | 一次完整任务的全局唯一 ID |
| session_id | 所属会话 |
| step_id | 当前编排步骤 ID |
| model | 使用的模型型号 |
| prompt_tokens / completion_tokens | token 消耗 |
| tool_calls | 本轮调用过的工具列表 |
| latency_ms | 该步骤耗时 |
| error_code | 错误码,无则为空 |
| llm_decision | 模型本轮的关键决策内容 |
这份日志不是给人肉翻的,而是喂给监控系统的。生产上我们接的是 OpenTelemetry 那一套,也用过专门的 LLM 可观测平台;如果团队规模小,先用 JSON 行日志 + 采集器打点也能起步。重点是字段必须从一开始就设计完整,后面补字段往往是痛苦的重构。
5.2 指标、告警和 SLO:像守核心系统一样守 Agent
Agent 的稳定性指标要分成两层:
- 系统层:响应成功率、P95 延迟、队列积压数、模型调用错误率、工具调用错误率、成本消耗。
- 业务层:任务完成率、首次回复有效占比、人工介入率、用户反馈满意度。
业务层指标最难做,但也最重要。我常用的人工介入率(Human Takeover Rate)是一个非常好的健康信号:Agent 处理过程中有多少比例的任务需要转人工?这个数字一旦持续升高,就说明 Agent 在真实场景下的能力不达标,应该触发专项优化而不是继续扩大流量。
告警阈值要结合实际场景。比如我会对“工具失败率超过 5%”告警,因为工具是确定性组件,经常失败说明要么接入质量差,要么下游系统有问题。而“模型响应延迟 P95 超过 15 秒”,通常意味着 prompt 里塞入了过多上下文,需要优化检索逻辑。
5.3 评测集:Demo 可以现场发挥,生产必须回归测试
Demo 可以由最懂模型的人现场发挥,生产则必须有自动化的回归评测。没有评测集,你的 Agent 每次改一版 prompt、换一个模型、调一下参数,都可能引入未知的行为退化。
我建议从第一天就建设评测集,哪怕只有五十条。主要包括三类:典型业务问题、边界与异常问题、安全与越权问题。每次改动后跑一遍全部评测,对比正确率、回复质量和工具调用行为。
离线评测的判定方式也不能只靠人看。实践中我会用两种方式混合:规则判定(比如必须包含某个关键元素、禁止输出某类敏感词)加上 LLM-as-Judge 打分,定期人工抽检。抽检结果再反馈到评测集,形成闭环。
在线评测则走灰度:先给 5% 流量,对比 Agent 和人工/旧系统的表现,再看人工介入率和用户反馈,一切正常再逐步扩大比例。这个流程和传统上线流程没本质区别,但你需要提前把两类流量的对比埋点做好,否则灰度期间数据缺失等于白跑。
5.4 安全护栏与权限边界的工程化实现
最后一个大问题,是 Agent 的权限边界。很多 Demo 里,Agent 把什么都说了,把什么工具都能调了;生产环境绝不允许这样。
落地时我至少会做四件事:
- 角色权限:Agent 能读取哪些数据、调用哪些工具,必须和用户本身的 RBAC 权限一致。不能在模型层绕过权限。
- 敏感信息脱敏:日志和上下文中的手机号、身份证、密钥等要做脱敏,不能在 Trace 里明文落盘。
- 输出护栏:给 Agent 配一份“允许输出”和“禁止输出”的规则集,例如不得输出内部系统访问地址,不得直接给出他人的个人隐私信息。
- 审计日志:高风险工具调用、审批动作、权限变更等,必须全量记录并长期保留。
再补充一点,针对 Agent 被恶意提示注入的情况,工具层也要有一层防御:不让模型模型输出直接变成 SQL 或系统命令执行。我能看到太多团队把“Agent 调 API”做成了“Agent 拼接 SQL 字符串”,这是妥妥的生产事故预埋。正确的做法是尽量让工具接收结构化参数,哪怕这个参数来自模型的 JSON 输出,也要在工具执行层做白名单校验。模型只能说“执行查询”,不能决定“查询哪张表”之外的东西。
安全这道坎做不好,Agent 不只是“上线拉胯”,而是“上线即事故”。它不会像 Demo 那样只需要酷,它必须可管理。
6. 从 0 到 1 的落地顺序:几个让我少走弯路的实战忠告
6.1 建议的落地路线:窄场景 → 工程化闭环 → 平台化复制
我已经吃够了“一步到位、平台先行”的亏。Agent 生产落地的正确顺序,不是先搭一个大平台再找场景,而是先用一个窄场景打通全链路,工程能力成熟后再横向复制。
第一阶段的场景,要符合三个条件:高频、低风险、边界清晰。比如“企业知识库问答”“IT 工单的初步分类与路由”,这些场景只读不写,即使 Agent 出错,后果也可控,适合做生产环境的第一个磨刀石。
第二阶段,在这个窄场景里把前面提到的东西全部补上:无状态运行、幂等工具、分层记忆、可观测、评测集、安全护栏。同时把人工兜底路径跑通,让业务方对这个系统建立真实信任。
第三阶段,当单个场景完成闭环,再把它抽象成可配置的 Agent 编排平台,让后续场景通过配置的方式快速接入。这时候平台的组件都是被验证过的,新人接入一个场景的成本才会真正降下来。
6.2 几个导致返工的认知误区
一是“先选 Agent 框架,再做业务场景”。框架只是编排工具,真正决定上线成败的是围绕框架的工程能力。先想清楚业务要解决什么问题,再选框架也不迟。
二是“模型强,就不需要做评测和护栏”。这是我在很多团队里见过的最危险的想法。模型越强,出错时的影响面越大。评测和护栏跟模型能力没有关系,它们是系统的安全网。
三是“全自动才是最终目标”。生产里有很多场景,人机协同的体验和稳定性远好于全自动。比如高危操作前加一道审批,复杂任务先生成方案让人确认再执行。这种“有限自主权”的模式,业务方接受度更高,风险也更可控。
四是“ Agent 的记忆无穷无尽,不用管”。这个问题我已经反复强调过,真实生产里上下文会被塞爆,记忆要有生命周期、检索和合规。
最后一个我自己的体会:做 Agent 生产落地,最难得不是写 prompt、调模型,而是做一个让模型和业务都愿意信任的工程系统。模型负责聪明的部分,工程负责兜住笨的部分。每次上线前,我都会问团队一个问题:如果这个 Agent 今天抽风出现最离谱的行为,我们能不能拦住、能不能发现、能不能快速恢复?三个问题都能给出明确答案,才是真正“可以上线”的 Agent。