ARTICLE DETAIL

资讯详情

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

Agent生产落地四道坎:Schema治理、上下文工程、并发稳定性与可观测评测

Agent生产落地四道坎:Schema治理、上下文工程、并发稳定性与可观测评测 1. 从Demo到生产Agent落地为什么总在同一个地方翻车做Agent项目的人大概都经历过这个循环花两天搭出一个Demo工具调用丝滑多轮对话流畅演示给老板看的时候掌声雷动。然后进入生产环境用户量一上来各种诡异问题开始冒出来——工具调用参数偶尔缺字段、模型返回的JSON解析失败、同一个请求重试三次结果完全不同、线上出了badcase但日志里什么都查不到。这不是某个团队的问题而是整个Agent工程化阶段的普遍现状。我过去一年多参与过几个Agent项目从零到生产的完整过程踩过的坑基本可以归到四道坎上工具调用的Schema治理、上下文与记忆的工程化、并发与稳定性、可观测与评测。这四件事在Demo阶段几乎不需要考虑但到了生产环境每一件都能让项目停摆。这篇文章不打算讲Agent是什么、LLM怎么调API这种入门内容。我想聊的是当你已经能跑通一个Agent Demo接下来要让它扛住真实流量、真实用户、真实业务的时候具体该怎么做。涉及到的技术点包括工具调用的Schema设计Zod Schema、JSON Schema、Agent框架的编排选择LangGraph等、并发模型、可观测体系搭建以及评测闭环。适合已经动手做过Agent项目、正在往生产推进的开发者。先说一个反直觉的结论Agent上线拉胯大部分时候不是模型能力不够而是工程约束没做好。模型再强你给它一个模糊的工具描述、一个没有校验的返回值、一个没有超时控制的外呼它照样会给你制造事故。下面按四道坎逐一拆解。2. 第一道坎工具调用的Schema治理2.1 为什么Schema是Agent生产化的第一道门槛工具调用是Agent区别于普通Chatbot的核心能力。用户说一句话Agent决定调用哪个工具、传什么参数、拿到结果后怎么继续。这个链路里Schema就是Agent和外部世界之间的契约。Demo阶段大家怎么写工具大概是这样定义一个函数写个docstring扔给框架自动转成工具描述。参数类型差不多就行。必填可选模型自己猜。返回值格式返回个字符串让模型自己理解。这套做法在Demo里能跑因为Demo的输入是你精心设计的。但生产环境的用户输入是发散的模型对工具的理解是有偏差的外部API的返回是不可控的。三个不可控叠加结果就是工具调用失败率飙升。我见过一个典型案例一个查询订单状态的工具参数是order_id类型string。Demo时用户说帮我查一下订单12345模型正确提取。上线后用户说我上周买的那个东西到哪了模型开始编造order_id或者干脆传个空字符串。更糟的是工具本身没有参数校验空字符串直接打到下游API返回一个500Agent拿到500之后开始胡言乱语。2.2 Zod Schema与JSON Schema的分工现在主流Agent框架基本都支持用Schema定义工具。这里要区分两个层面开发侧用Zod Schema。Zod是TypeScript生态里的运行时校验库它的价值在于你定义一次既能得到TypeScript类型又能在运行时做校验。对于Agent工具来说这意味着你可以在工具真正执行前拦截掉参数不合法的调用。import { z } from zod; const QueryOrderSchema z.object({ order_id: z.string().regex(/^\d{10,20}$/, 订单号必须是10-20位数字), user_id: z.string().uuid(用户ID格式不正确), query_type: z.enum([status, logistics, refund]).default(status), }); type QueryOrderParams z.infertypeof QueryOrderSchema;这段代码做了几件事约束了order_id的格式、要求user_id必须是UUID、给query_type设了默认值。当模型传过来的参数不满足这些约束时Zod会抛出明确的错误你可以把这个错误信息返回给模型让它重新生成参数。传给模型侧用JSON Schema。模型API接受的是JSON Schema格式的工具描述。Zod Schema可以通过zod-to-json-schema这类库自动转换。关键是转换出来的JSON Schema要足够清晰让模型能理解每个参数的含义和约束。{ name: query_order, description: 查询用户的订单状态、物流信息或退款进度。当用户询问订单相关问题时使用此工具。, parameters: { type: object, properties: { order_id: { type: string, description: 订单号10-20位纯数字。如果用户没有提供不要猜测先向用户询问。, pattern: ^\\d{10,20}$ }, user_id: { type: string, description: 当前登录用户的ID从会话上下文中获取不要向用户询问。, format: uuid }, query_type: { type: string, enum: [status, logistics, refund], description: 查询类型。status查订单状态logistics查物流refund查退款进度。默认status。 } }, required: [order_id, user_id] } }注意description的写法。工具描述不是写给人类看的文档是写给模型看的提示词。每一句话都要考虑模型看到这句话会怎么理解会不会产生歧义如果用户没有提供不要猜测先向用户询问这种指令能显著降低模型编造参数的概率。2.3 参数校验失败后的重试策略Schema校验失败之后怎么办直接报错给用户是最差的选择。合理的做法是把校验错误信息结构化地返回给模型让它重新生成参数。但这里有个坑不能让模型无限重试。我建议设置最大重试次数通常2-3次超过之后降级到人工兜底或者返回一个友好的错误提示。async function executeToolWithRetry( toolName: string, params: unknown, maxRetries: number 2 ) { for (let i 0; i maxRetries; i) { const result schema.safeParse(params); if (result.success) { return await actualToolExecute(result.data); } if (i maxRetries) { return { error: PARAM_VALIDATION_FAILED, message: 参数校验失败${result.error.message}。请检查后重新提供。, }; } // 把错误信息返回给模型让它重新生成 params await askModelToFixParams(toolName, params, result.error); } }还有一个容易被忽略的点工具返回值的Schema。很多团队只校验入参不校验出参。下游API返回的数据结构可能变化Agent拿到一个预期外的结构后后续推理全乱。建议对工具返回值也做一层校验和归一化确保Agent看到的永远是稳定的结构。2.4 工具描述的粒度控制工具数量多了之后另一个问题浮现模型选错工具。你有20个工具模型经常在相似的几个之间反复横跳。解法有两个方向。一是合并粒度把功能相近的工具合并成一个用参数区分行为。比如query_order和query_logistics可以合并成query_order加一个query_type参数。二是分层路由先用一个轻量模型做意图分类确定大类之后再加载对应的工具子集。我个人的经验是单次对话暴露给模型的工具不要超过10个。超过这个数量模型的选择准确率会明显下降。如果业务确实需要很多工具就做分层别一股脑全塞给模型。3. 第二道坎上下文与记忆的工程化3.1 上下文窗口不是越大越好现在模型动辄128K、200K上下文很多人觉得窗口够大全塞进去就行。实际做Agent的时候你会发现上下文越长模型注意力越分散关键信息越容易被淹没。我做过一个对比测试同一个多轮对话任务把完整历史塞进去 vs 只保留最近5轮加摘要后者的任务完成率反而更高。原因很简单无关的历史信息成了噪声模型在噪声里找信号准确率自然下降。Agent的上下文管理要解决三个问题放什么、放多少、怎么压缩。放什么系统提示词、当前任务相关的工具描述、最近几轮对话、关键的工具调用结果、用户的核心诉求。放多少根据任务复杂度动态调整简单任务少放复杂任务多放。怎么压缩超出预算时对早期对话做摘要保留关键实体和决策点。3.2 记忆分层短期、长期、工作记忆Agent的记忆不该是一坨。我习惯把它分成三层工作记忆是当前任务执行过程中的临时状态。比如Agent正在处理一个退款流程当前走到哪一步、已经收集了哪些信息、还缺什么。这部分放在上下文里任务结束就丢弃。短期记忆是当前会话的历史。用户这次对话说了什么、Agent做了什么、工具返回了什么。这部分需要压缩和摘要不能无限增长。长期记忆是跨会话的用户偏好和历史。用户上次买过什么、偏好什么沟通风格、之前遇到过什么问题。这部分通常存在外部存储里按需检索注入上下文。interface AgentMemory { working: { currentTask: string; steps: TaskStep[]; collectedSlots: Recordstring, unknown; }; shortTerm: { recentTurns: Turn[]; summary: string; keyEntities: Entity[]; }; longTerm: { userProfile: UserProfile; pastIssues: Issue[]; preferences: Recordstring, string; }; }分层的价值在于每一层有不同的生命周期和存储策略。工作记忆跟着任务走短期记忆跟着会话走长期记忆跟着用户走。混在一起管理必然混乱。3.3 上下文压缩的实操方法上下文压缩不是简单截断。截断会丢失关键信息导致Agent失忆。我常用的压缩策略是实体提取加摘要。把早期对话里的关键实体订单号、金额、时间、用户诉求提取出来连同一段简短摘要替换掉原始对话。这样既保留了关键信息又大幅缩短了长度。工具结果的结构化保留。工具返回的原始JSON可能很长但Agent真正需要的往往只是其中几个字段。在工具执行层做一次裁剪只把关键字段返回给模型。滑动窗口加锚点。保留最近N轮完整对话加上更早对话的摘要再加上一个锚点——通常是用户最初的核心诉求。锚点确保Agent不会在长对话中跑偏。提示压缩后的上下文要定期做回归测试。我遇到过压缩策略上线后某些边缘case的完成率下降原因是摘要把关键约束条件给概括没了。压缩不是无损的必须用评测集验证。3.4 记忆写入的时机与去重长期记忆的写入时机很关键。写太早用户还没表达完整偏好记了个半截写太晚会话结束了信息丢了。我的做法是在任务完成节点和会话结束节点各写一次。任务完成时写入任务相关的结构化信息会话结束时写入用户偏好和未解决问题。写入前做一次去重避免同一个偏好被反复记录。去重可以用向量相似度也可以用简单的规则匹配。对于结构化字段比如用户偏好语言直接覆盖更新对于非结构化信息比如用户抱怨过的问题用相似度阈值判断是否已存在。4. 第三道坎并发、超时与稳定性4.1 Agent的并发模型和普通服务有什么不同普通Web服务的并发模型很成熟请求进来查数据库返回结果每个请求独立。Agent的并发要复杂得多因为一个Agent请求内部可能包含多次模型调用和多次工具调用这些调用之间有依赖关系且耗时差异巨大。一个用户请求可能触发1次意图识别模型调用200ms、3次工具调用每次500ms-2s、1次结果汇总模型调用1s。总耗时可能3-5秒。如果并发上来模型API的速率限制、工具下游的承载能力、Agent服务自身的线程池任何一环都可能成为瓶颈。更麻烦的是流式输出。Agent通常需要流式返回给用户但流式意味着连接要保持打开对服务端的连接管理提出更高要求。4.2 超时控制的分层设计Agent链路长超时控制必须分层。我通常设三层层级超时对象建议值超时后行为L1单次模型调用30s重试1次仍失败则降级L2单次工具调用10s返回超时错误给模型让模型决定L3整个Agent请求60s返回部分结果加提示L1的超时要考虑模型的首token时间和总生成时间。流式调用下首token超时和总超时要分开设。L2的工具超时要根据下游服务的SLA来定但一定要设不能让Agent无限等。L3是兜底防止某个请求卡死占用资源。async function callWithTimeoutT( fn: () PromiseT, timeoutMs: number, fallback: T ): PromiseT { const timeoutPromise new PromiseT((resolve) setTimeout(() resolve(fallback), timeoutMs) ); return Promise.race([fn(), timeoutPromise]); }4.3 模型API的速率限制与重试模型API基本都有速率限制RPM/TPM。Agent场景下一个用户请求可能消耗多次调用速率限制很容易被打满。应对策略令牌桶限流加指数退避重试。在Agent服务内部维护一个令牌桶控制对模型API的调用速率。遇到429错误时指数退避重试但要注意重试次数上限避免雪崩。还有一个技巧把非关键的模型调用降级到小模型。比如意图识别、参数提取这类任务用便宜快速的小模型只有最终的结果生成用大模型。这样能显著降低对大模型的调用压力。4.4 幂等性与状态管理Agent执行过程中可能因为超时、网络抖动等原因中断。如果中断后重试要保证不会重复执行副作用操作比如重复下单、重复发消息。做法是给每个工具调用分配一个幂等键通常是会话ID 任务ID 步骤序号。工具执行前先检查这个键是否已执行过已执行则直接返回缓存结果。状态管理方面Agent的中间状态要持久化。不能只放在内存里否则服务重启状态就丢了。我通常用Redis存工作记忆用数据库存任务执行记录。这样即使服务重启也能从断点恢复。注意状态持久化会带来序列化开销。对于高频的中间状态更新要评估是每次都持久化还是批量持久化。我的经验是关键决策点必须持久化中间过程可以批量。5. 第四道坎可观测与评测闭环5.1 Agent的可观测和传统APM有什么不同传统APM关注的是请求耗时、错误率、QPS。Agent的可观测要复杂得多因为Agent的正确性不是二元的。一个请求返回了200不代表Agent做对了。它可能调用了错误的工具、传了错误的参数、给出了错误的答案但整个链路没有报错。所以Agent的可观测要覆盖几个层面链路追踪每个请求的完整调用链包括模型调用、工具调用、决策节点。每个节点记录输入、输出、耗时、token消耗。决策记录Agent在每一步为什么做这个选择。是哪个工具描述影响了它的选择是上下文里的哪句话让它改变了策略质量指标任务完成率、工具调用准确率、参数提取准确率、用户满意度。这些指标需要专门的评测体系来采集。5.2 关键埋点与数据结构我通常会在这些位置埋点interface AgentTrace { traceId: string; sessionId: string; userId: string; startTime: number; endTime: number; steps: AgentStep[]; totalTokens: { input: number; output: number }; finalStatus: success | partial | failed; userFeedback?: positive | negative; } interface AgentStep { stepIndex: number; type: model_call | tool_call | decision; input: unknown; output: unknown; durationMs: number; tokens?: { input: number; output: number }; error?: string; metadata: Recordstring, unknown; }这套结构能支撑大部分排查场景。线上出badcase时通过traceId拉出完整链路看Agent在哪一步做了错误决策是工具描述的问题、上下文的问题、还是模型本身的问题。5.3 评测集的建设与迭代没有评测集的Agent项目就是在裸奔。评测集不是一次性的要持续迭代。我的做法是从线上真实流量里采样人工标注形成评测集。初期可能只有几十条随着项目推进逐步扩充到几百条。评测集要覆盖正常case、边缘case、对抗case用户故意刁难。评测的执行可以自动化。用脚本跑评测集对比Agent的输出和标注的期望输出。对于开放式任务可以用LLM as Judge做自动评分但要注意Judge本身的偏差定期用人工校准。interface EvalCase { id: string; input: string; context?: Recordstring, unknown; expectedToolCalls?: ToolCall[]; expectedOutput?: string; category: normal | edge | adversarial; difficulty: 1 | 2 | 3 | 4 | 5; }每次修改工具描述、调整上下文策略、更换模型版本都要跑一遍评测集对比指标变化。没有评测的优化都是盲改。5.4 从badcase到修复的闭环可观测的最终价值是形成修复闭环。一个完整的闭环是线上badcase → trace定位问题环节 → 归因工具描述/上下文/模型/下游 → 修复 → 加入评测集 → 回归验证 → 上线监控。这个闭环跑通之后Agent的质量会持续提升。我见过太多团队badcase来了就临时改prompt改完也不记录下次遇到类似问题又从头排查。没有闭环就是在原地打转。归因的时候要区分是模型能力问题还是工程问题。模型能力问题可能需要换模型或加few-shot工程问题则是Schema、上下文、超时这些可以代码修复的。大部分时候工程问题的占比更高。6. 框架选型LangGraph这类编排工具到底解决了什么6.1 从手写状态机到图编排早期做Agent很多人是手写状态机定义几个状态写一堆if-else决定下一步。简单场景能跑复杂场景就变成意大利面条。LangGraph这类图编排框架的价值在于把Agent的执行流程显式地表达成图。节点是执行单元模型调用、工具调用、条件判断边是流转逻辑。这样流程可视、可调试、可修改。但要注意框架不是银弹。LangGraph解决的是编排问题不解决Schema治理、并发、可观测这些问题。这些还是得自己搭。选框架的时候要清楚它能帮你省掉哪部分工作哪部分还得自己来。6.2 什么场景适合用图编排不是所有Agent都需要图编排。我的判断标准是流程有明确的多步骤且步骤之间有依赖关系 → 适合需要条件分支根据中间结果决定下一步 → 适合需要人工介入节点human-in-the-loop → 适合简单的单轮工具调用 → 不需要直接函数调用更简单图编排的代价是增加了抽象层调试的时候要多一层心智负担。如果业务逻辑本身很简单硬上图编排是过度设计。6.3 自研编排与框架编排的取舍自研编排的好处是可控坏处是要自己处理状态管理、错误恢复、并发控制这些脏活。框架编排的好处是这些脏活有人帮你干了坏处是遇到框架的边界情况时你可能要读源码或者绕路。我的建议是先用框架快速验证遇到框架解决不了的问题时评估是改框架还是自研。大部分团队最终会是混合模式核心流程用框架特殊逻辑用自研节点嵌入。7. 上线前的检查清单与灰度策略7.1 上线前的硬性检查项在Agent上线前我会过一遍这个清单所有工具的入参和出参都有Schema校验所有外部调用都有超时和重试关键操作有幂等保护上下文有压缩策略且经过评测验证有完整的链路追踪和日志有评测集且当前版本通过评测有降级方案模型不可用、工具不可用时怎么办有速率限制和熔断机制任何一项没过都不建议上生产。7.2 灰度发布的节奏控制Agent的灰度不能像普通服务那样按流量比例切。因为Agent的行为有随机性同样的输入可能得到不同的输出。灰度的时候要关注按用户维度灰度而不是按请求维度。同一个用户的所有请求走同一个版本避免体验不一致。灰度比例要小步慢走。1% → 5% → 20% → 50% → 100%每个阶段观察足够长的时间。Agent的问题往往在特定输入模式下才暴露需要足够的样本量。灰度期间加强监控。除了常规指标还要看任务完成率、工具调用失败率、用户负反馈率这些Agent特有的指标。7.3 回滚预案Agent的回滚比普通服务复杂因为可能有状态残留。回滚预案要包括版本回滚切回上一个稳定版本状态处理正在执行的任务怎么处理是让它跑完还是中断数据一致性已经产生的副作用下单、发消息怎么处理我通常会在Agent服务里保留最近两个版本的能力回滚时直接切流量不做重新部署。状态方面正在执行的任务让它跑完新任务走旧版本。8. 一些踩坑之后的个人体会做Agent生产落地这一年多最大的体会是Agent的工程复杂度被严重低估了。大家看到的是Demo的惊艳看不到的是背后Schema治理、上下文管理、并发控制、可观测这一整套工程体系。第二个体会是评测集是Agent项目的生命线。没有评测集你所有的优化都是凭感觉。有了评测集你才能量化每一次改动的效果才能知道是进步还是退步。第三个体会是不要追求一步到位。四道坎不用同时解决可以按优先级来。我的建议顺序是先做Schema治理收益最直接再做可观测没有观测就没法优化然后做上下文管理最后做并发和稳定性这个可以随着流量增长逐步完善。最后一个体会Agent的很多问题根因不在Agent本身而在它依赖的外部系统。工具的下游API不稳定、返回格式不统一、错误码不规范这些问题会通过Agent放大。做Agent生产化的时候要顺带把依赖的外部系统也治理一遍。这个领域变化很快框架在迭代模型在升级最佳实践也在演进。但工程化的底层逻辑是不变的清晰的契约、可控的流程、可观测的执行、可验证的质量。把这四件事做好Agent从Demo到生产的距离就会短很多。
返回列表