开篇:从 AI 对话 Demo 到可演进的 Agent 平台
这两年 AI 圈最热闹的词,一个是“AI”,一个是“Agent”。市面上 Demo 满天飞,今天一个聊天机器人,明天一个自动写周报的工具,后天又冒出个能帮你订机票的智能体。但说句实话,绝大多数 Demo 活不过三个月——因为从“能跑通的演示”到“能撑住真实场景的平台”,中间隔着一条巨大的鸿沟。我自己就是从这个坑里爬过来的,所以想写一个系列,记录一下我是怎么把一个 AI 对话 Demo 慢慢磨成一个可演进的 Agent 平台的。这一篇是开篇,先把整体思路、技术选型和最常见的那些“为什么这样做”讲清楚。
这篇文章适合谁?如果你刚接触 Agent 开发,手头有个能对话的 Demo 但不知道怎么往下走;或者你已经用 LangChain、Semantic Kernel 之类的框架搭过原型,想搞清楚生产环境里的 Agent 到底需要哪些组件;再或者你就是单纯好奇,为什么有的 AI 产品看起来聪明,有的看起来像个只会复读机的玩具——这篇文章都能给你一个比较明确的答案。我不打算写那种全是概念的文章,咱们直接聊代码、聊架构、聊踩坑。
先说清楚一件事:Demo 和平台不是量的区别,是质的区别。Demo 的核心目标是“证明可行”,平台的核心目标是“保证可用”。后者需要你考虑并发、状态、可观测性、容错、版本演进、权限控制,这些东西在 Demo 阶段完全不需要碰,但一旦你想让 Agent 真正干活,一个都躲不掉。接下来我会从设计思路、核心细节、实操过程、问题排查、平台化建议这五个角度,把这个演进路径完整的拆出来。
1. 整体设计与思路拆解:为什么 Demo 必须死一次
1.1 从“对话模型”到“Agent 平台”的本质转变
先给 Agent 下一个我自己的定义:Agent = 大模型(决策大脑)+ 工具(手脚)+ 记忆(经验库)+ 执行环境(工作台)。Demo 阶段的 Agent 往往只做到了第一项,也就是接一个大模型 API,做个聊天界面,用户说什么就回什么。这种 Demo 本质上就是一个“加了皮肤的大模型聊天框”,它没有工具调用能力、没有上下文管理、没有任务规划,更不用说多轮对话之间的状态保持。
你可能会说,这不就是 Chatbot 吗?对,它就是一个 Chatbot,还不是 Agent。Chatbot 和 Agent 的区别在于:Chatbot 是被动的,用户问一句它答一句;Agent 是主动的,它能根据目标拆解任务、调用外部工具、根据执行结果调整策略、甚至在没有用户介入的情况下完成一个多步骤的复杂任务。
从技术架构上说,对话 Demo 只需要四个部分:前端界面、后端 API、大模型接口、简单的会话存储。但 Agent 平台最少要有八层:前端交互层、网关层、任务编排层、工具注册与调度层、记忆管理层、模型路由层、执行沙箱层、可观测性层。这不是我拍脑袋想出来的,而是我在实际演进过程中一步步加上的——每加一层,都是因为线上出了事故或者用户提出了真实需求。
1.2 演进式架构的核心思路:不为明天过度设计
在搭建 Agent 平台之前,我一直在想一个问题:要不要一开始就上个完整的微服务架构?Kubernetes、服务网格、事件驱动、多租户隔离,全部一步到位?答案是否定的。如果你的第一版就把这些全部安排上,那大概率在三个月之后还是在写配置,而不是在调 Agent。
我的做法是演进式架构:保持单体应用的简单性,但通过模块边界的清晰划分,为后续拆分成独立服务留好口子。具体到代码层面,就是把对话管理、工具调度、记忆管理、模型接口封装成独立的内部模块,每个模块有明确的接口定义,模块之间通过事件或接口调用通信,而不是直接共享数据库表。这样做的优势在于:第一版可以快速上线验证业务逻辑,后期当某个模块成为瓶颈或者需要独立扩展时,再拆出去才是水到渠成的事情。
这里有个关键的架构决策:接口设计要面向接口而非面向实现。举个例子,模型路由层不应该写死“调用 OpenAI”,而是应该抽象出一个LLMProvider接口,后面接 OpenAI、Claude、本地部署的开源模型、甚至自训练的微调模型,都是通过实现这个接口来接入。我在前期吃过亏,就是把模型调用写死在业务逻辑里,后来想切换模型时,几乎重写了整个对话模块。
1.3 为什么“工具调用”是 Agent 平台的核心分水岭
如果说大模型是 Agent 的大脑,那工具调用就是 Agent 的手脚。为什么我把工具调用列为分水岭?因为一个只能生成文本的 Agent,和一个能查数据库、调 API、操作文件系统、发 HTTP 请求的 Agent,完全是两个物种。前者只能做信息整理和文本生成,后者才能真正参与业务流程。
实现工具调用的技术路径,主流是 Function Calling。大模型在生成回复时,会同时输出一个结构化的工具调用请求(通常是 JSON 格式),包含工具名称和参数。后端拿到这个请求后,执行对应的工具函数,把执行结果返回给大模型,大模型再基于结果生成最终回复。这个机制看起来简单,但实际落地时有很多坑:工具数量多了以后,模型的选择准确率会下降;工具参数复杂时,模型容易生成格式错误的 JSON;工具执行结果太长时,上下文会被撑爆。
我在设计工具调度层的时候,做了一个关键决策:给每个工具加上独立的超时控制、错误处理策略和重试机制。某个工具挂了,不能拖垮整个 Agent 的执行流程。具体怎么实现,后面实操部分会详细讲。
2. 核心细节解析与实操要点:Demo 阶段就要打好的地基
2.1 消息协议设计:别让前端和后端各说各话
很多人觉得消息协议是小事,后端返回 JSON 就好了,前端拿到渲染就完事。但在 Agent 场景里,消息不是一次性的,它是一个持续演进的数据结构。我第一版 Demo 的消息协议就设计得太简单了,就是一个{role: "user" | "assistant", content: string}的结构。后来要支持流式输出、工具调用状态、多模态内容、内容引用、结构化中间结果,改协议改到头大。
我建议在最开始就设计一个稍微超前一点的消息结构:
{ "id": "msg_123456", "session_id": "session_789", "role": "user", "content": [ {"type": "text", "text": "帮我查一下上个月的销售数据"}, {"type": "tool_call", "tool_name": "query_database", "arguments": {"table": "sales", "month": "2024-06"}} ], "metadata": { "token_usage": 1200, "latency_ms": 350, "model": "gpt-4o-mini", "timestamp": "2025-01-15T10:30:00Z" } }这个结构有四个好处:第一,content是数组而不是字符串,将来要加图片、表格、工具调用结果,直接加数组元素就行,不用破坏原有结构;第二,metadata可以塞调试信息和性能指标,生产排查问题非常有用;第三,session_id从第一天就留着,后面做记忆管理和多轮上下文都是基于这个字段;第四,整个结构本身就支持流式更新的语义。
这个协议我强烈建议在 Demo 阶段就定好,不然后面改起来的成本是你想象不到的。我见过不少项目,前后端接口改了五六版,每次都是因为消息格式缺字段,这都是前期设计偷懒的代价。
2.2 大模型接入层的抽象:换模型不换业务代码
接入层抽象这事,我再强调一次,因为它实在太重要了。我见过太多人在代码里直接写openai.ChatCompletion.create(...),然后整个项目就死绑在 OpenAI 上了。不是说 OpenAI 不好,而是说你把自己绑死了以后,想换模型的时候就会非常痛苦——而现在的模型市场变化太快了,今天一个开源模型登顶,明天一个国产模型免费开放,后天你老板说预算不够要换更便宜的,你能直接换吗?
我的做法是定义一个LLMProvider接口,然后为每家服务商写一个实现:
class LLMProvider(ABC): @abstractmethod def chat(self, messages: list[dict], tools: list[dict] | None = None, **kwargs) -> ChatResult: pass @abstractmethod def chat_stream(self, messages: list[dict], tools: list[dict] | None = None, **kwargs) -> Iterator[StreamChunk]: pass然后 OpenAIClient、AnthropicClient、LocalLlamaClient、ZhipuClient 各写一个实现类。注意,ChatResult里不能只存响应文本,还要存 token 用量、延迟、模型名、结束原因(是正常结束还是达到最大 token 数被截断)。这些元信息在 Agent 场景里非常关键,因为你要根据结束原因决定是否要触发后续动作。
这个抽象层还有一层隐藏作用:做模型路由的灰度切换。比如新来一个模型,你可以在线上先切 5% 的流量过去,跑几天看下效果指标,再逐步扩大。如果没有这层抽象,这种灰度操作几乎无从谈起。
2.3 上下文管理:为什么你的 Agent 记忆力那么差
聊上下文管理之前,先想一个生活场景:你去一家餐厅吃饭,第二次去的时候服务员说“王先生您好,还是老位置吗?”你会觉得这家店服务很好。但如果服务员每次都问“您要吃点什么?”你就觉得好像也没啥差别。Agent 也是一样的,没有上下文管理的 Agent,每次对话都是“第一次见面”,用户会觉得自己在跟一个失忆症患者聊天。
Demo 阶段的上下文管理很简单:把对话记录全部塞给大模型。但到了生产环境,你会遇到两个问题:第一是成本问题,上下文越长,token 费用越高;第二是效果问题,上下文过长时,大模型会“迷失在长篇中”,注意力分散,记住开头忘掉结尾。
我推荐的方案是三层上下文管理:
- 短期上下文:当前对话窗口最近 5-10 轮消息,这是模型直接能看到的内容。
- 中期记忆:对历史对话做摘要,每隔几轮对话,对前面的内容做一次总结,放入上下文。
- 长期记忆:把用户偏好、关键事实、历史决策提取成结构化的 Memory Record,存入向量数据库,需要时召回。
基于长期记忆的召回我用的方案是:把关键信息抽取成{key: value}对,例如{"用户偏好": "喜欢简明回答,不喜欢长篇大论", "常用工具": ["SQL查询", "图表生成"]},然后用嵌入模型转成向量存储。召回时通过用户当前输入向量检索最相关的一批记忆记录,拼接到上下文中。
这个结构在 Demo 阶段不需要做全,但至少要把基础数据结构设计好,知道后面要往哪个方向扩展。换句话说:不要在 Demo 阶段就做三层上下文,但要在数据结构里留session_id、memory_refs这些字段,为后面铺路。
3. 实操过程与核心环节实现:从单体到多智能体
3.1 单体架构阶段:先让一个 Agent 跑通完整闭环
我第一次真正搭建 Agent 平台的时候,没有急着上微服务,而是在一个单体应用里写了完整的 Agent 执行逻辑。这个单体应用的核心是一个AgentRuntime类,它把“人机对话”、“工具调用”、“上下文管理”串成了一个循环:
class AgentRuntime: def __init__(self, provider: LLMProvider, registry: ToolRegistry, memory: MemoryManager): self.provider = provider self.registry = registry self.memory = memory async def run(self, session_id: str, user_input: str): # 1. 加载历史消息 messages = await self.memory.get_context(session_id) messages.append({"role": "user", "content": user_input}) # 2. 最多循环 5 次,防止无限工具调用 for _ in range(5): response = await self.provider.chat(messages, tools=self.registry.schema()) if response.tool_calls: # 3. 执行工具调用 for tool_call in response.tool_calls: result = await self.registry.execute(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) continue # 4. 如果没有工具调用,直接返回最终回复 await self.memory.save(session_id, messages) return response.content raise AgentLoopExceededError("工具调用次数超出限制")这个循环是 Agent 执行的核心模式,你去看 LangChain、AutoGPT、BabyAGI 等框架,底层的逻辑都差不多。关键点就在第 5 行的for _ in range(5)——这一步是防止 Agent 陷入无限循环的保险丝。我见过太多 Agent 死在无限工具调用上,没有这个限制的 Agent,线上会把自己跑死,账单也会飞起。
工具调度层的ToolRegistry我用了注册器模式,给每个工具加装饰器注册:
@registry.register(name="query_database", description="查询业务数据库,支持 SQL 和自然语言") async def query_database(database: str, question: str, **kwargs): """执行数据库查询,返回结果给大模型""" ... @registry.register(name="send_email", description="发送邮件给指定收件人") async def send_email(to: str, subject: str, body: str, **kwargs): """发送一封邮件""" ...第 5 行的self.registry.schema()是工具调用的关键——它会把所有注册的工具转成大模型需要的 JSON Schema 格式。这一步看起来简单,但其实有不少细节,比如工具的description写得越清楚,模型选择正确工具的概率越高;参数的格式越规范,模型生成参数时越不容易出错。我在工具描述上吃过不少亏,一开始写的都是“查询数据库”这种模糊描述,后来学乖了,每个工具描述都写成 “当用户想查询销售数据时使用此工具,支持按日期范围、客户维度、产品维度过滤” 这种带明确使用场景的描述,工具选择准确率一下子就上来了。
3.2 工具调用链路:数据怎么在模型和工具之间流转
工具调用链路是 Agent 平台里最容易出错的一环,我要重点讲一下数据流转的过程。假设用户说“帮我查一下最近一周的销售数据,然后画个折线图发到邮件里”,这个需求会拆成两个工具调用:query_database和send_email,中间可能还有一个generate_chart。
整个流转过程是这样的:
- 大模型对用户输入进行意图识别,输出两个工具调用的 JSON,格式类似
[{"tool": "query_database", "args": {"time_range": "2024-06-01~2024-06-07"}}, {"tool": "generate_chart", "args": {"chart_type": "line", "x_field": "date", "y_field": "sales"}}]。 - 后端拿到这个 JSON 后,先校验工具是否存在、参数是否合法,然后顺序执行这些工具(有依赖关系时还可能并行执行)。
- 每个工具执行完返回一个结构化的结果,比如
query_database返回一个 JSON 数组,generate_chart返回一张图片的 URL。 - 后端把这些工具结果按顺序追加到消息队列中,重新发给大模型,让大模型基于所有执行结果生成最终的用户回复。
这里有个细节:工具结果追加的时候,一定要带上tool_call_id,因为大模型要求工具结果和工具调用一一对应。如果漏了对应关系,模型会报 “Invalid tool_call_id” 的错误,整个链路就断了。我见过太多人在这上面踩坑,因为框架帮你做了这个关联,一旦你手写链路,就容易漏掉。
执行链路设计环节,有个优化我很推荐:工具并行调用。大模型一次输出多个工具调用时,如果它们之间没有依赖关系,就可以通过 asyncio.gather 并行执行。比如“查数据库”和“查天气”这两个动作没有依赖,可以同时跑,能大幅缩短整体响应时间。但有依赖关系时必须串行,比如“先查询数据库拿到数据,再生成图表”,后者依赖前者的返回结果。
3.3 从单体到多智能体:拆分的时机与方式
很多人一上来就要做多智能体架构,这是典型的过度设计。单体 Agent 能解决的问题,没必要拆成多智能体。但你一定会遇到单体解决不了的时候。我自己判断的标准是:当两个能力之间需要隔离开的时候,就该拆了。
举个例子,我发现客服助手这个 Agent 同时负责“查订单”和“表单填写”两件事,但“查订单”的模型配置需要低延迟、高准确率,“表单填写”则更需要创造力和上下文理解。这两个能力挤在同一个模型配置下,经常互相干扰——要么查订单太慢,要么表单生成质量差。于是我把它们拆成两个 Agent,分别配置不同的模型和参数,再通过一个调度器,根据用户意图把请求路由到对应的 Agent。
多智能体架构下,我需要加一个协调层。协调层有两种模式:中心化编排(有一个主 Agent,其他都是子 Agent,主 Agent 负责任务拆解和结果汇总)和去中心化协商(多个 Agent 通过消息互相协商)。实际生产中,大部分场景用中心化编排就够了。我在这个阶段引入了一个轻量级的任务分派机制:主 Agent 接收用户请求,拆解成子任务,分派给对应的子 Agent,子 Agent 完成后把结果返回,主 Agent 整合最终答案。
这个过程代码如下:
// 使用 TypeScript 示例,生产环境我用的就是这个模式 type AgentUnit = { name: string; capability: string; handle: Function; modelConfig: ModelConfig; }; class Orchestrator { private agents: AgentUnit[] = []; register(agent: AgentUnit) { this.agents.push(agent); } async dispatch(input: string): Promise<string> { // 第一步:让主 Agent 决定如何拆解任务 const plan = await this.plan(input); // 第二步:按计划分派给子 Agent 执行 const results = await Promise.all( plan.tasks.map(async (task) => { const agent = this.findAgent(task); if (!agent) return `未找到能处理任务 ${task} 的 Agent`; return agent.handle(task); }) ); // 第三步:汇总所有结果 return this.synthesize(input, results); } }这里我强烈建议,也不要一上来就写 Orchestrator。先把单体跑稳,线上有真实流量了,再根据流量日志去判断哪些请求是单 Agent 搞不定的,再引入多 Agent。拆分的时机一定是“业务驱动”,而不是“技术炫酷驱动”。
3.4 状态管理与持久化:对话中断了怎么恢复
Agent 平台的稳定性,很大程度上取决于状态管理做得好不好。Demo 阶段,对话状态都存在内存里,进程一重启就全丢了。但在生产环境,用户可能跟你聊到一半去开会,回来继续聊,你不能让他重新说一遍。
我的做法是把状态分成两层:对话层状态(消息历史、会话 ID)和执行层状态(当前任务进度、已完成的子任务、待执行的工具调用)。对话层状态存 Redis,过期时间设为 7 天;执行层状态也存 Redis,但过期时间设为 24 小时,因为一个任务的执行周期一般不会超过一天。
Redis 里存执行状态的数据结构,我用的是 JSON 字符串,格式长这样:
{ "session_id": "sess_123", "task_id": "task_456", "status": "executing", "current_step": "waiting_for_tool_result", "pending_tool_calls": [ {"tool": "query_database", "args": {...}, "status": "done", "result": "..."}, {"tool": "generate_chart", "args": {...}, "status": "pending"} ], "retry_count": 2, "deadline": "2025-01-16T10:00:00Z" }有了这个状态结构,当用户的请求因为异常中断时,新请求进来会先从 Redis 读取执行层状态,判断上一个任务是否完成。如果发现status是executing,并且current_step是waiting_for_tool_result,就可以从上次断点继续,而不是重新开始。这个机制在生产环境中非常有用,尤其是遇到网络抖动、后端重启、模型超时这些意外情况时。
4. 常见问题与排查技巧实录:那些让我深夜加班的 Bug
4.1 模型突然不调工具了,怎么排查
这是一个让我印象很深的线上事故。本来 Agent 跑得好好的,用户说“帮我查天气”,模型正确地调用了weather_api工具。突然有一天开始,模型一直给出相似的回答,就是不调用工具,像是退化成了一个纯聊天机器人。排查了很久,最后定位到是升级了模型版本后,工具描述格式被改动,模型解析工具 Schema 出了问题,所以它“干脆不调用工具了”。
排查工具调用异常,我的经验是:首先拉日志看模型输出的原始响应。如果原始响应里有tool_calls字段,但没有触发执行,那问题出在工具执行层;如果原始响应里根本没有tool_calls字段,那问题大概率出在模型侧——不是模型被降级了,就是工具 Schema 格式不对或者描述语义不清晰。
另外,我还遇到过一种情况:工具描述里出现了"enabled": true这种配置,某个版本的模型会把这个字段理解为“这个工具总是返回成功”,然后就不执行了。这个问题的教训是:工具 Schema 尽量保持简单,不该加的字段不要加,加了反而会混淆模型的理解。
4.2 Agent 陷入无限循环,预算被烧掉一半
有一次上线后,我们监控发现某个 Agent 的 token 消耗异常,一小时烧掉了平时一天的量。排查发现是 Agent 陷入了“工具调用失败 → 重试 → 再失败 → 再重试”的循环,模型不断地查询一个已经确定查不到数据的数据库,因为工具返回的错误信息没有告诉模型“这个数据不存在,不要重复查询”。
这个问题的根源,是工具的错误信息写得太笼统。当时query_database工具返回的错误是"Error: query failed",这个错误没有告知模型具体是哪个字段查不到、数据范围是什么、有没有替代方案。模型不知道数据不存在,所以一直换种方式再查。
解决方式:所有工具必须返回结构化的错误信息,明确告知错误类型、错误原因、建议替代方案。比如:
{ "error": "no_data", "message": "在 2024-06-01 至 2024-06-07 范围内没有销售数据", "suggestion": "尝试扩大时间范围,例如查询 2024-05-01 至 2024-06-07" }这样模型一看就知道“哦,数据不存在,不是查询姿势不对”,会停止重试,转而给出合理的用户回复。这个改动上线之后,Agent 的不再浪费 token 在无意义的重复尝试上。
4.3 上下文爆炸:对话超过 20 轮之后,Agent 变笨了
对话轮数增加后,Agent 的效果变差,这是很多人会遇到的问题。我遇到过最夸张的情况:用户和 Agent 聊了大概 30 轮,Agent 开始忘记用户最开始提到的约束条件,开始自己编造信息,回答内容前后矛盾。
问题出在上下文管理上——把所有历史消息都塞给模型,到了 30 轮时,上下文里的历史消息已经几千个 token,模型处理起来已经“糊了”。而且 token 费用也在飙升。
我的解决办法是:引入“上下文压缩”策略。每轮对话结束时,对话历史超过一定长度(我的阈值是 10 轮),就启动一次压缩:把前面的对话摘要成一个简短的summary消息,放回上下文。同时保留最近 5 轮原始完整内容。
# 压缩前 [system: 你是客服助手] [user: 我订单一直没到] [assistant: 您好,请稍等,我查一下订单状态...] [user: 订单号是 ABC123] [assistant: 好的,查到了,订单正在配送中...] ... 省略中间 25 轮 ... [user: 那优惠券什么时候能用] # 压缩后 [system: 你是客服助手] [user: <summary> 用户反映订单 ABC123 延迟,我们已经告知正在配送中。用户对配送时效不满,已经安抚并解释了原因。最近用户询问优惠券使用时间。 </summary>] [assistant: 好的,我来查一下优惠券的使用规则...]这个策略的效果立竿见影:上下文长度控制住了,模型不会被海量历史消息干扰,响应速度也快了不少。代价是摘要过程多了一次模型调用,但对比 token 费用的节省,这个代价完全可接受。
4.4 排查工具集(Agent 可观测性)
很多 Agent 平台最缺的就是可观测性——你不知道用户跟 Agent 聊天的时候内部发生了什么,出了问题只能靠猜。所以我在搭建平台的过程中,坚持在第一天就埋点,第一天就上追踪。每个会话、每个工具调用、每次模型请求都有独立的 trace ID,通过 trace ID 能把整条链路串起来。
我用的是 OpenTelemetry 标准,把每个 Agent 执行环节的数据都打到集中式日志平台。具体记录以下字段:
- 会话 ID、用户 ID(如果有)
- 模型的输入/输出 token 数量、响应延迟
- 工具名称、工具调用的输入参数、返回值状态
- 上下文压缩前后的大小对比
- 每个环节的时间戳、耗时
- 错误类型、错误堆栈
有了这些数据,我排查问题的效率提高了 10 倍不止。遇到问题不再靠猜,而是直接看链路追踪,找到瓶颈或报错点。这里也建议大家别想着等平台大了再补可观测性,补的代价远高于一开始就加。埋点是最容易又最值钱的投入。
5. 走向平台化的扩展建议:Agent 平台还需要哪些组件
5.1 模型路由与降级策略:别让大模型当单点
Agent 平台里的大模型调用是最容易出问题的一环:限流、超时、内容审核拦截、价格变动、模型下架。如果你把大模型当作单点来用,是真正的风险,后果是线上必出故障。
我的方案是做一个模型路由层,支持三个策略:
- 按优先级路由:优先使用高质量模型,失败后自动降级到备用模型。
- 按成本路由:简单问题用便宜模型,复杂问题用贵模型。
- 按能力路由:需要视觉理解的任务走多模态模型,需要代码生成的任务走代码能力强的模型。
实现降级逻辑的代码非常简单:
class ModelRouter: def __init__(self, strategies: List[RoutingStrategy]): self.strategies = strategies async def route(self, task: Task) -> ModelProvider: for strategy in self.strategies: provider = await strategy.select_provider(task) if provider: return provider raise NoAvailableModelError() async def execute_with_fallback(self, task: Task): primary = await self.route(task) try: return await primary.chat(task.messages, tools=task.tools) except Exception as e: logger.warning(f"主模型 {primary.name} 失败,尝试备用模型: {e}") backup = await self.route_with_exclude(task, exclude=primary.name) return await backup.chat(task.messages, tools=task.tools)这个模块不长,但它能解决的问题非常大。一个很典型的场景是,高峰期大模型服务商限流,没有路由层的平台直接报错给用户;有了路由层的平台可以自动切换备用模型,用户完全没有感知。
5.2 Agent 评测体系:没有评测就没有优化
做 Agent 平台最难的,不是写代码,而是“怎么判断这次改动是变好了还是变坏了”。自然语言生成不像传统软件,有一个明确的 pass/fail 标准。你改了一版提示词,输出看起来好像更流畅了,但也可能丢失了某些关键信息。这种情况下没有评测体系,根本无法判断。
我的做法是把评测体系拆成三层:
- 确定性指标:工具调用成功率、错误率、响应延迟、Top-1 工具选择准确率。这些是硬指标,可以直接跑自动化测试统计。
- 内容质量指标:回答的相关性、完整性、格式正确性、是否包含幻觉。这些指标目前主要依赖人工评估,但可以借助另一套大模型来辅助打分。
- 业务指标:用户满意度、任务转化率、用户留存率。这些是最终衡量 Agent 价值的指标。
具体到实现,我开发了一套评测 runner,每两天自动跑一次回归测试集。测试集里包含 100 个典型用户问题,覆盖知识问答、工具调用、复杂任务拆解、多轮对话这几种场景。评测 runner 把 Agent 的输出和期望结果做对比,输出一个分数。如果分数下降,说明这次改动是有问题的,需要回滚或者调整。
评测集我用一个 JSON 配置管理:
[ { "id": "test_001", "scenario": "查询订单状态", "user_input": "帮我查一下订单 12345 的配送状态", "expected_tools": ["query_order"], "expected_keywords": ["配送中", "预计送达"], "max_allowed_latency_ms": 3000, "min_score": 0.85 } ]这个配置里expected_tools是关键,它验证“Agent 是否选择了正确的工具”而不是仅仅验证“最后说什么”。这个评测体系帮我在后续改版中避免了很多次“感觉上变好了但实际上关键能力退化了”的陷阱。
5.3 平台的 API 化:让 Agent 的能力被其他系统集成
平台要真正落地,必须 API 化。我第一版 Agent 只有前端界面能用,后来技术团队、运营团队都来问“能不能让你们 Agent 的能力接入到我们的系统里”,我只能说抱歉,然后回去加班加 API。
API 设计我遵循几个原则:
- 所有接口都是异步的:因为 Agent 执行可能耗时数秒,不可能让调用方一直阻塞等待。
- 通过 Webhook 通知任务完成:Agent 执行完成后,回调调用方传入的 Webhook 地址。
- 有权限控制:API Key 管理,API Key 对应不同的工具权限和模型权限。
举个例子,用户查询插件的调用流程是:
POST /v1/agent/task # 创建任务,传入会话 ID、用户输入 => 返回 { "task_id": "task_789", "status": "accepted" } Agent 处理完毕后 POST https://customer-system.com/webhook/agent-result # 回调通知 => 返回执行结果、会话上下文、工具调用记录API 化之后,Agent 平台从一个“只能内部使用的工具”变成了“可供全公司甚至外部系统调用的服务”,这个转变是平台化最重要的体现。这个方向,也是我认为 Agent 平台未来真正产生价值的地方——它不再只是个别产品里的一项功能,而是整个业务体系里可以重复调用的通用能力单元。
从 Demo 到平台,不是一个单纯的技术升级,而是一整套思维方式的变化。Demo 阶段你关心的是“能不能跑通”,平台阶段你关心的是“能不能长期稳定地跑、能不能被别人集成、能不能在模型和工具快速迭代的情况下持续可用”。这条路没有终点,模型在变、工具在变、用户需求在变,Agent 平台也必须跟着变。这一篇先把整体架构、核心环节和常见问题讲清楚了,后面的系列文章里,我会展开聊工具调用深挖、评测体系建设、多智能体协作模式、记忆机制的进阶玩法这些方向。如果你也在折腾 Agent 平台,欢迎一起交流你在架构演进中遇到的问题——因为我踩过的坑,很可能你正在踩。