尧图精选

Agent生产落地四道坎:工具调用、Schema、可观测性与并发架构实战

🕒 发布时间:2026/10/2 15:26:13 📁 来源:尧图网络
1. 从 Demo 到生产Agent 落地为什么总在同一个地方翻车做过 Agent 项目的人大概都有过这种体验本地跑 Demo 的时候工具调用丝滑、推理链路清晰、输出结果惊艳给老板演示完信心满满。结果一上生产环境用户量稍微起来一点各种问题就像约好了一样集中爆发——工具调用超时、Schema 校验失败、上下文爆炸、并发一上来整个链路雪崩。这不是某个团队的问题而是整个行业在 Agent 工程化过程中反复踩到的同一批坑。我自己在过去一年多里参与过几个企业级 Agent 项目从客服工单自动处理到内部知识库问答再到多步骤业务流程编排几乎每个项目都经历了“Demo 惊艳、上线拉胯”的完整周期。踩过的坑多了之后我慢慢意识到一件事Agent 从 Demo 到生产之间的距离不是靠换一个更强的 LLM 模型就能填平的。它本质上是一个系统工程问题涉及工具调用的可靠性、Schema 的严格约束、可观测性的建设、以及并发架构的设计。这篇文章想做的事情很具体把 Agent 生产落地过程中最核心的四道坎拆开来讲清楚每一道坎都给出可操作的工程解法。不管你是刚开始做 Agent 项目的开发者还是正在为线上 Agent 稳定性头疼的工程师都能从里面找到可以直接抄作业的方案。文章会涉及工具调用、Schema 设计、可观测性、并发处理这些关键词但不会停留在概念层面而是落到具体的代码结构、参数配置和排查技巧上。先说一个我观察到的现象很多团队在 Demo 阶段用的是“能跑就行”的思路工具定义随手写、错误处理基本没有、日志打得到处都是但没有结构。这种思路在单用户、低频率的场景下没问题但一旦进入生产环境用户请求的多样性、工具返回的不确定性、以及并发带来的资源竞争会把这些隐患全部放大。所以四道坎的第一道就是工具调用的可靠性问题。2. 第一道坎工具调用为什么总在关键时刻掉链子2.1 工具调用的本质与常见失败模式工具调用Tool Calling是 Agent 区别于普通 Chatbot 的核心能力。简单说就是 LLM 根据用户意图决定调用哪个外部函数、传什么参数然后由执行层去实际调用再把结果返回给 LLM 继续推理。这个过程听起来很直接但实际落地时失败模式非常多。我整理过我们线上系统一个月的工具调用失败日志大致可以分成几类参数缺失或类型错误占了大头差不多四成工具执行超时占两成半工具返回结果解析失败占两成剩下的是权限、限流和其他杂项。这个分布很有意思它说明大部分问题不是 LLM 不够聪明而是工程约束没做到位。举个例子我们有一个查询订单状态的工具参数是订单号和用户 ID。Demo 阶段 LLM 每次都能正确提取这两个参数但上线后发现当用户说“帮我看看我上周买的那个东西到哪了”这种模糊表达时LLM 有时候会只传用户 ID 不传订单号或者把订单号传成用户 ID。工具执行层拿到不完整的参数要么报错要么返回错误结果整个对话就断了。2.2 用 Schema 把工具定义变成硬约束解决这个问题的核心思路是把工具定义从“自然语言描述”升级为“结构化 Schema 约束”。这里说的 Schema不是随便写个 JSON 示例就行而是要用严格的类型系统来定义每个参数的类型、必填性、取值范围和默认值。如果你用 TypeScript 技术栈Zod Schema 是目前比较成熟的选择。它能在运行时做参数校验而且类型推导很自然。比如上面那个订单查询工具用 Zod 定义大概是这样的import { z } from zod; const OrderQuerySchema z.object({ userId: z.string().uuid(用户ID必须是合法的UUID格式), orderId: z.string().regex(/^ORD\d{12}$/, 订单号格式为ORD加12位数字).optional(), timeRange: z.enum([last_week, last_month, last_quarter]).default(last_month), status: z.enum([pending, shipped, delivered, cancelled]).optional(), }); type OrderQueryParams z.infertypeof OrderQuerySchema;这个 Schema 做了几件事userId 强制要求 UUID 格式orderId 可选但必须符合特定正则timeRange 有默认值status 限定枚举。当 LLM 生成的参数不符合这些约束时Zod 会在执行前就抛出明确的错误而不是等到工具内部才失败。但光有 Schema 还不够关键是怎么让 LLM 知道这些约束。我的经验是在工具描述里要把 Schema 的约束用自然语言再强调一遍尤其是那些容易出错的字段。比如在工具描述里写“orderId 是可选参数格式为 ORD 加 12 位数字如果用户没有明确提供订单号请不要猜测而是先询问用户。”这种双重约束——Schema 做硬校验描述做软引导——能显著降低参数错误率。2.3 工具执行层的容错与降级设计Schema 校验通过之后工具实际执行时还会遇到各种问题。最常见的是超时和外部服务不可用。我们的做法是在工具执行层加一层“熔断降级”机制。具体来说每个工具调用都设置独立的超时时间默认 5 秒对于查询类工具可以放宽到 10 秒。超时后不是直接报错而是返回一个结构化的降级结果告诉 LLM“工具暂时不可用请基于已有信息继续推理或者告知用户稍后重试”。这样 LLM 有机会走备用路径而不是整个对话中断。另外对于幂等性工具比如查询类我们会做一次自动重试重试间隔用指数退避第一次 500ms第二次 1.5s。非幂等工具比如创建订单则不做自动重试而是返回明确的错误码让 LLM 决定是否要告知用户重新操作。这里有个细节值得注意工具返回的结果也要做 Schema 校验。我们遇到过外部服务返回的 JSON 字段类型和文档不一致的情况比如文档说 count 是数字实际返回了字符串 10。如果直接把这个结果喂给 LLM可能导致后续推理出错。所以我们在工具返回层也加了一层 Zod 校验把结果标准化之后再交给 LLM。实操心得工具定义不要贪多。我见过一个项目定义了三十多个工具结果 LLM 经常选错工具。后来我们精简到十二个核心工具把一些低频功能合并成带参数的模式工具选择准确率从 70% 提升到了 92%。工具数量控制在 15 个以内每个工具的参数不超过 5 个是一个比较舒服的范围。3. 第二道坎Schema 设计不只是类型定义更是契约管理3.1 Schema 在 Agent 链路中的三重角色很多人把 Schema 理解成“参数类型定义”这个理解太窄了。在 Agent 生产系统里Schema 其实扮演着三重角色对 LLM 来说它是工具能力的说明书对执行层来说它是输入校验的守门员对下游服务来说它是接口契约的载体。这三重角色如果有一重没做好整个链路就会出问题。我见过一个典型的反面案例团队用 JSON Schema 定义了工具参数但描述字段写得很随意LLM 经常误解参数含义。比如一个叫 “query” 的参数描述只写了“查询内容”LLM 有时候传用户原话有时候传关键词有时候传结构化查询语句。下游服务拿到这些五花八门的输入处理逻辑变得极其复杂。后来我们把参数拆成 “queryText” 和 “queryType” 两个字段queryType 用枚举限定为 “keyword” 或 “natural_language”问题才解决。3.2 从 Zod Schema 到 LLM 可理解的工具描述Zod Schema 是给代码看的但 LLM 需要的是自然语言描述。这两者之间需要一个转换层。我们的做法是写一个工具注册函数把 Zod Schema 自动转换成 LLM 能理解的工具描述格式同时保留人工补充说明的字段。interface ToolDefinitionT extends z.ZodType { name: string; description: string; schema: T; hints?: string[]; // 人工补充的提示 execute: (params: z.inferT) Promiseunknown; } function registerToolT extends z.ZodType(def: ToolDefinitionT) { const jsonSchema zodToJsonSchema(def.schema); const paramDescriptions Object.entries(jsonSchema.properties).map( ([key, value]: [string, any]) { const required jsonSchema.required?.includes(key) ? 必填 : 可选; return - ${key} (${required}): ${value.description || 无描述}; } ); const fullDescription [ def.description, 参数说明, ...paramDescriptions, ...(def.hints || []), ].join(\n); return { ...def, llmDescription: fullDescription }; }这个注册函数做了几件事自动提取 Zod Schema 的字段信息标注必填/可选拼接人工提示。这样每个工具的定义既保持了代码层面的类型安全又生成了 LLM 友好的描述文本。3.3 Schema 版本管理与向后兼容生产环境还有一个容易被忽视的问题Schema 会变。业务需求调整、下游接口升级、参数增减都会导致 Schema 变更。如果处理不好就会出现“LLM 按旧 Schema 生成参数执行层按新 Schema 校验”的错位问题。我们的做法是给每个工具 Schema 加版本号并且在工具注册时保留最近三个版本的兼容逻辑。当 LLM 生成的参数不符合当前版本时先尝试用旧版本 Schema 解析如果能解析成功就自动做一次参数迁移然后继续执行。同时打点记录版本不匹配的情况用于后续分析。const schemaVersions new Mapstring, z.ZodType[](); function resolveParams(toolName: string, rawParams: unknown) { const versions schemaVersions.get(toolName) || []; for (const [index, schema] of versions.entries()) { const result schema.safeParse(rawParams); if (result.success) { if (index 0) { console.warn(工具 ${toolName} 使用了旧版本 Schema v${versions.length - index}); } return result.data; } } throw new Error(工具 ${toolName} 参数校验失败无匹配的 Schema 版本); }这个机制在我们一次下游接口升级中救了命。当时订单查询工具的参数从orderId改成了orderIds数组但 LLM 的提示词还没更新仍然生成单个orderId。兼容层自动把单个值包装成数组业务没有中断我们从容地更新了提示词。注意事项Schema 变更一定要走灰度。我们现在的流程是新 Schema 先上线兼容层观察一周的版本不匹配日志确认 LLM 已经适应新格式后再移除旧版本兼容。直接切换 Schema 是生产事故的常见诱因。4. 第三道坎可观测性决定你能不能定位问题4.1 Agent 可观测性与传统服务的差异传统后端服务的可观测性主要看三个东西日志、指标、链路追踪。Agent 系统也需要这些但多了一层“推理过程”的可观测性。你不仅要看到工具调用成功还是失败还要看到 LLM 为什么选择这个工具、推理链路的每一步是什么、上下文是怎么变化的。我们刚开始做 Agent 的时候日志就是简单地把 LLM 输入输出打出来。结果线上出问题的时候面对几千行日志完全无从下手。后来我们重新设计了可观测性方案核心思路是“结构化事件流”把 Agent 执行的每一步都变成一个结构化事件包含时间戳、事件类型、输入、输出、耗时、以及关联的 trace ID。4.2 结构化事件流的设计与落地一个完整的 Agent 执行链路我们定义了这些事件类型用户输入接收、意图识别、工具选择、参数生成、Schema 校验、工具执行、结果解析、LLM 推理、最终输出。每个事件都记录关键字段比如工具选择事件会记录候选工具列表和最终选择参数生成事件会记录原始参数和校验后参数。interface AgentEvent { traceId: string; spanId: string; parentSpanId?: string; eventType: string; timestamp: number; duration?: number; payload: Recordstring, unknown; error?: { code: string; message: string }; } function emitEvent(event: AgentEvent) { // 写入结构化日志系统 logger.info(JSON.stringify(event)); // 同时上报到指标系统 metrics.increment(agent.event.${event.eventType}, { error: event.error ? true : false, }); if (event.duration) { metrics.histogram(agent.duration.${event.eventType}, event.duration); } }这套事件流上线后我们排查问题的效率提升非常明显。以前一个工具调用失败要翻半天日志才能定位到是参数问题还是执行问题。现在直接按 traceId 过滤整条链路一目了然哪个环节耗时最长、哪个参数校验失败、LLM 当时看到了什么上下文全都清清楚楚。4.3 关键指标与告警阈值设置可观测性不只是事后排查更重要的是事前告警。我们基于事件流定义了十几个核心指标每个指标都有明确的告警阈值。下面这张表是我们线上系统实际使用的指标配置可以直接参考指标名称含义告警阈值处理优先级tool_call_success_rate工具调用成功率低于 95% 持续 5 分钟P1schema_validation_fail_rateSchema 校验失败率高于 5% 持续 10 分钟P1llm_first_token_latency_p99LLM 首 token 延迟 P99高于 3 秒P2agent_task_completion_rate任务完成率低于 85% 持续 15 分钟P1context_token_usage_p95上下文 token 使用量 P95高于模型上限 80%P2tool_timeout_rate工具超时率高于 3% 持续 5 分钟P1retry_rate重试率高于 10% 持续 10 分钟P2这些阈值不是拍脑袋定的是我们根据历史数据统计出来的基线再留出合理波动空间。比如工具调用成功率正常情况在 98% 以上低于 95% 说明有系统性问题必须马上看。实操心得告警不要只看单点要看趋势。我们有一次工具调用成功率从 99% 缓慢降到 96%单看每个时间点都没触发告警但趋势很明显。后来加了一个“环比下降超过 2%”的告警规则提前发现了下游服务性能退化的问题。5. 第四道坎并发架构决定 Agent 能不能扛住真实流量5.1 Agent 并发与传统 API 并发的本质区别传统 API 的并发处理相对简单每个请求独立无状态水平扩容就行。Agent 的并发要复杂得多因为一次 Agent 任务可能包含多轮 LLM 调用和多次工具调用整个链路是有状态的而且 LLM 调用本身耗时较长通常在几秒到几十秒之间。我们做过压测一个简单的 Agent 任务平均需要 3 轮 LLM 调用和 2 次工具调用端到端耗时约 8 秒。如果用传统的同步阻塞模型单实例并发能力非常有限。假设每个请求占用一个线程 8 秒100 个并发就需要 100 个线程资源消耗很大而且 LLM 调用大部分时间在等网络 IO线程利用率很低。5.2 异步编排与连接池管理解决并发问题的核心思路是异步化。把 Agent 执行链路拆成多个异步步骤用事件驱动的方式串联起来。LLM 调用和工具调用都走异步 IO不阻塞线程。这样单实例可以轻松支撑几百个并发任务。我们用 LangGraph 做编排它天然支持异步节点和状态管理。每个 Agent 任务是一个独立的图执行实例节点之间通过状态传递数据。LLM 调用节点和工具调用节点都是异步的执行器用连接池管理 HTTP 连接避免频繁建连开销。from langgraph.graph import StateGraph, END import asyncio import aiohttp class AgentState(dict): messages: list tool_results: list current_step: str async def llm_node(state: AgentState): async with aiohttp.ClientSession() as session: async with session.post( LLM_ENDPOINT, json{messages: state[messages]}, timeoutaiohttp.ClientTimeout(total30), ) as resp: result await resp.json() return {messages: state[messages] [result[message]]} async def tool_node(state: AgentState): tool_calls extract_tool_calls(state[messages][-1]) tasks [execute_tool_async(call) for call in tool_calls] results await asyncio.gather(*tasks, return_exceptionsTrue) return {tool_results: results} graph StateGraph(AgentState) graph.add_node(llm, llm_node) graph.add_node(tool, tool_node) graph.add_conditional_edges(llm, should_continue, {tool: tool, end: END}) graph.add_edge(tool, llm)这个结构的关键点是LLM 节点和工具节点都是 async 函数多个工具调用用 asyncio.gather 并发执行。连接池通过 aiohttp 的 ClientSession 管理每个任务复用连接减少握手开销。5.3 限流、排队与优先级调度异步化解决了资源利用率问题但还需要限流和排队来保护系统。我们的做法是在 Agent 入口加一个令牌桶限流器根据系统当前负载动态调整速率。同时维护一个优先级队列把任务分成高、中、低三档高优先级任务比如付费用户、关键业务优先调度。import asyncio from collections import deque class PriorityTaskQueue: def __init__(self, max_concurrent: int): self.max_concurrent max_concurrent self.semaphore asyncio.Semaphore(max_concurrent) self.queues {1: deque(), 2: deque(), 3: deque()} async def submit(self, task, priority: int 2): future asyncio.Future() self.queues[priority].append((task, future)) asyncio.create_task(self._process()) return await future async def _process(self): async with self.semaphore: for priority in [1, 2, 3]: if self.queues[priority]: task, future self.queues[priority].popleft() try: result await task() future.set_result(result) except Exception as e: future.set_exception(e) return这个队列实现有几个细节用 Semaphore 控制最大并发数优先级从高到低扫描队列每个任务执行完释放信号量后触发下一轮处理。实际生产中我们还会根据队列长度动态调整 max_concurrent队列积压超过阈值时自动扩容实例。注意事项并发控制不只是限制数量还要考虑超时和取消。我们遇到过用户关闭页面后 Agent 任务还在后台跑的情况浪费资源。后来在任务提交时绑定了客户端连接状态连接断开就取消任务。这个细节在压测时看不出来但生产环境能省不少资源。6. 四道坎之外的工程细节那些文档不会告诉你的经验6.1 上下文管理与 Token 预算控制Agent 执行多轮之后上下文会越来越长token 消耗快速增长。我们统计过一个平均 5 轮的 Agent 任务上下文 token 数从第一轮的 800 涨到最后一轮的 4000 多。如果不加控制不仅成本高而且 LLM 的推理质量会下降。我们的做法是给每个 Agent 任务设置 token 预算比如 8000 token。每轮推理前检查剩余预算如果不够就触发上下文压缩把早期的工具调用结果摘要化只保留关键信息。摘要用一个小模型来做成本低速度快。async def compress_context(messages: list, budget: int) - list: total_tokens count_tokens(messages) if total_tokens budget: return messages # 保留最近 3 轮完整对话 recent messages[-6:] older messages[:-6] # 对早期对话做摘要 summary await summarize_with_small_model(older) return [{role: system, content: f历史对话摘要{summary}}] recent这个策略在我们线上运行了半年任务完成率没有下降但平均 token 消耗降低了 35%。6.2 工具调用的幂等性与副作用管理有些工具是有副作用的比如创建工单、发送通知、修改数据。这类工具在重试时必须保证幂等性。我们的做法是给每个有副作用的工具调用生成一个唯一的 idempotency key由 LLM 在生成参数时一并生成执行层用这个 key 做去重。const CreateTicketSchema z.object({ title: z.string().min(5).max(100), description: z.string().min(10), priority: z.enum([low, medium, high]), idempotencyKey: z.string().uuid(), }); async function createTicket(params: z.infertypeof CreateTicketSchema) { const existing await db.findTicketByIdempotencyKey(params.idempotencyKey); if (existing) { return { ticketId: existing.id, deduplicated: true }; } const ticket await db.createTicket(params); return { ticketId: ticket.id, deduplicated: false }; }idempotencyKey 由 LLM 生成有个好处同一个用户请求如果因为超时重试LLM 会生成相同的 key因为上下文相同从而保证幂等。但这里有个坑如果 LLM 每次生成的 key 不一样幂等就失效了。所以我们在提示词里明确要求“idempotencyKey 必须基于用户请求内容生成相同请求必须生成相同 key”并且在 Schema 校验时检查 key 的格式。6.3 多城市 Schema 标记与分站场景的特殊处理我们有一个项目涉及多城市服务不同城市的工具参数和业务规则有差异。比如查询门店信息北京和上海的门店编码规则不同。这种场景下Schema 需要做分站标记。我们的方案是在工具 Schema 里加一个 cityCode 字段并且在工具描述里说明不同城市的参数差异。执行层根据 cityCode 路由到不同的下游服务。同时在 LLM 提示词里注入当前用户所在城市的信息引导 LLM 生成正确的 cityCode。const StoreQuerySchema z.object({ cityCode: z.enum([BJ, SH, GZ, SZ]), storeName: z.string().optional(), district: z.string().optional(), }).refine( (data) { if (data.cityCode BJ data.district) { return /^[东城|西城|朝阳|海淀|丰台|石景山]/.test(data.district); } return true; }, { message: 北京地区 district 必须是标准行政区名称 } );这个 refine 做了跨字段校验确保 district 和 cityCode 匹配。实际运行中LLM 偶尔会生成不匹配的组合Schema 校验会拦截并返回明确错误LLM 收到错误后会重新生成。实操心得分站场景下提示词里的城市信息一定要放在靠前的位置并且用醒目的格式标注。我们试过把城市信息放在系统提示词末尾LLM 经常忽略。后来改成在用户消息开头用【当前城市北京】这样的格式准确率明显提升。7. 从踩坑到填坑一套可复用的 Agent 生产检查清单7.1 上线前的自检项每次 Agent 项目上线前我们团队都会过一遍这份检查清单。这份清单是从多次生产事故中总结出来的每一条都对应一个真实踩过的坑。检查项检查内容通过标准工具 Schema 完整性每个工具是否有 Zod/JSON Schema 校验100% 覆盖工具描述清晰度参数说明是否包含格式、范围、示例人工评审通过超时配置每个工具是否有独立超时时间查询类 10s写入类 5s重试策略幂等工具是否配置重试非幂等是否禁止配置正确降级方案工具失败时是否有降级返回每个工具都有可观测性关键事件是否打点指标是否上报覆盖率 100%告警配置核心指标是否配置告警阈值至少 7 个核心指标并发限制是否有入口限流和队列管理压测验证Token 预算是否有上下文压缩机制压测验证幂等性有副作用的工具是否有 idempotency key100% 覆盖7.2 线上问题排查的通用路径线上出问题时我们有一套固定的排查路径基本能在 10 分钟内定位到根因。第一步看告警指标确定是哪个环节出问题。第二步按 traceId 拉取完整事件流看具体是哪一步失败。第三步检查 Schema 校验日志确认是不是参数问题。第四步看下游服务状态排除外部依赖问题。第五步如果以上都正常再看 LLM 的输入输出分析是不是提示词或上下文问题。这套路径的关键是事件流的完整性。如果可观测性没做好第三步之后就走不下去了。所以前面花大力气建设可观测性回报就体现在这里。7.3 持续迭代的节奏把控Agent 系统上线不是终点而是起点。我们的迭代节奏是每周看一次核心指标趋势每两周做一次失败案例复盘每月做一次 Schema 和提示词的优化。失败案例复盘特别重要我们会把线上失败的 trace 拿出来人工分析 LLM 的决策过程找出可以改进的点。有一次复盘发现LLM 在用户表达模糊时倾向于猜测参数而不是询问。我们在提示词里加了一条规则“当关键参数缺失且无法从上下文推断时必须先向用户确认不得猜测。”这条规则上线后参数错误率下降了 40%。这个内容后续还可以这样扩展如果你在做的是多 Agent 协作系统四道坎的解法需要叠加 Agent 间的通信协议和任务分配策略如果你用的是开源 LLM 自部署还需要考虑推理服务的并发能力和显存管理。但不管怎么扩展工具调用可靠性、Schema 约束、可观测性、并发架构这四个基础都是绕不过去的。我在实际项目中的体会是把这四件事做扎实Agent 的生产稳定性就能超过大部分团队剩下的就是持续迭代和优化了。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →