ARTICLE DETAIL

资讯详情

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

AI Agent从Demo到生产:四道工程化坎与解法

AI Agent从Demo到生产:四道工程化坎与解法 去年我参与推进了一个企业级知识库 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 进程因状态装在内存里导致会话错乱某个下游接口偶发 500Agent 重试三遍之后给你重复建了三笔工单用户聊到第 40 轮上下文窗口爆了Agent 把最早的业务约束“忘了”还有的 Agent 犯错了你根本无法定位它到底调了哪个工具输出了什么为什么选这一步。这些问题的共同点是在 Demo 里没人测或者测了也看不出来因为 Demo 总是“恰好走通”。工程化的本质就是把“恰好走通”变成“大部分情况下稳定走通少数失败时有明确的处理路径”。接下来我要拆的四道坎就是在补“稳定走通”和“失败处理路径”这两件事。2. 第一道坎扛并发不是加机器而是让 Agent 先“无状态化”2.1 有状态的 Agent 为什么无法水平扩展很多团队第一版 Agent 是直接把对话历史存在进程内的内存变量里比如一个 Python dictkey 是 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 fidem:{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, timeout10) # 执行成功后把结果写入幂等存储TTL 建议 24h 以上 await idem_store.set(key, result, ttl60 * 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 记忆拆成三层分别存储和治理层级存储内容存储介质生命周期短期记忆当前会话最近几轮原始消息RedisTTL 几小时会话结束即过期工作记忆当前任务拆解步骤、中间结果、待办清单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(fprofile:{user_id}) # 2. 语义检索历史对话片段 semantic_hits await self.vectors.search(query, top_k5) # 3. 合并为上下文 block交给 Agent 的 prompt 组装层 return merge(structured, semantic_hits)需要注意的是向量检索在几百条记忆时效果尚可到了几十万条就非常依赖切片质量与 Embedding 模型的领域适配。不要等到上线后才去做记忆调优最早一批业务数据出来就要开始验证检索召回率。4.4 记忆也是数据资产必须治理记忆长期化之后一定会牵扯到数据合规和权限问题。一个用户的信息不能被另一个用户检索到这是底线。实现上每条记忆都要带上 owner_id归属者和 scope可见范围检索时强制带上过滤条件。还要给用户提供“删除我的记忆”的入口团队内部最好有后台可以查看和清理异常记忆内容。我的经验是记忆模块越早设计权限边界越好否则等记忆量大了再改造迁移成本巨大。尤其是企业客户一旦发现自己会话里的敏感信息可能被其他用户检索到信任崩塌就再也救不回来了。5. 第四道坎让 Agent 可观测、可评测、可治理否则没人敢给你上线5.1 每个推理步骤都要有追踪记录包括成本Agent 与传统服务的最大差异是它的每次输出背后是一串不可完全预测的模型决策。没有追踪日志出问题你连复盘都无从下手。所以从第一天起我就要求 Agent 的每一次执行都必须输出结构化的 Trace 日志包含如下字段字段说明trace_id一次完整任务的全局唯一 IDsession_id所属会话step_id当前编排步骤 IDmodel使用的模型型号prompt_tokens / completion_tokenstoken 消耗tool_calls本轮调用过的工具列表latency_ms该步骤耗时error_code错误码无则为空llm_decision模型本轮的关键决策内容这份日志不是给人肉翻的而是喂给监控系统的。生产上我们接的是 OpenTelemetry 那一套也用过专门的 LLM 可观测平台如果团队规模小先用 JSON 行日志 采集器打点也能起步。重点是字段必须从一开始就设计完整后面补字段往往是痛苦的重构。5.2 指标、告警和 SLO像守核心系统一样守 AgentAgent 的稳定性指标要分成两层系统层响应成功率、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。
返回列表