1. 从一次 LLM 调用出发:架构选型问题的原点
最近被问得最多的问题就是 AI Agent 架构怎么选。每次有人把这个问题抛出来,我都会反过来先问一句:你的系统真的需要 Agent 吗?
这不是抬杠。我见过太多团队拿着一个"智能客服"的需求,第一天就开始调研分布式任务编排框架、多智能体通信协议、向量数据库选型,结果三个月后发现核心问题只是"模型老是答非所问"。反过来,也有团队把一个需要完整 Harness Runtime 支撑的复杂产品硬生生用 if-else 堆了出来,最后在并发、可观测性和上下文管理上全线崩溃。
AI Agent 架构不是一道选答题,而是一条演进路径。从一次最普通的LLM 调用开始,到一套完整的Harness Runtime,中间每一个层级都有它存在的理由、适用的场景和对应的代价。这篇文章把我自己的理解完整梳理一遍,包括每个阶段的实现细节、选型逻辑和踩过的坑,希望能帮你找到适合自己的那一档。
1.1 一次普通 LLM 调用,到底缺了什么
先说最基础的形态。假设你调了一次大模型 API,输入了一段 Prompt,得到了一段输出。就这么简单。逻辑上这就是一个"函数":给你输入,给你输出,没有副作用,没有中间状态。
但 Agent 之所以和"调用模型"不同,在于它引入了两个普通调用完全没有的东西:循环和状态。
举个例子。你问模型"帮我订一张周五从北京到上海的机票",模型能回答你将需要哪些信息、航空公司有哪些、怎么选航班。但如果你希望它真正把这件事办完,它需要:
- 自己判断"我现在需要调用一个查航班接口"(决策)
- 调用接口得到结果,再判断结果是否符合要求(循环)
- 如果航班信息不完整,它要想办法补全(状态修正)
- 判断什么时候该收手,返回最终结果(终止条件)
你看,一旦进入这种"感知-决策-行动"的循环,那个朴素的单次调用就不够用了。所以很多人说"Agent 就是 LLM + 工具 + 记忆",但这句话只说了表面。真正决定架构复杂度上限的,是环绕着这个循环的三件基础设施:流程怎么编排、状态怎么保存、运行过程怎么被观测。
1.2 最简 Agent:一个循环,一次函数调用
在工作中如果需要快速验证一个想法,我通常会写一个不到两百行的最小实现,而不是一上来就引框架。最简单的 Agent 长这样:
def run_agent(goal: str): context = {...} # 初始状态 for step in range(max_steps): action = llm.decide(prompt + str(context)) # 模型决定下一步动作 if action.type == "finish": return action.result result = execute(action) # 执行工具调用 context.update(result) # 更新状态 raise TimeoutError("max steps exceeded")这段代码的核心理解就三点:
第一,llm.decide这一步本质上是把"当前状态 + 任务目标 + 可用工具列表"打包成 Prompt,让模型输出一个结构化动作。第二,execute是真正干活的函数,它可以是查数据库、调外部 API、执行一段代码。第三,循环的出口必须明确,否则模型会在一个错误状态里反复打转,烧光你的 Token。
这样一个实现大概二百行就能跑通,对于 Demo、内部工具、个人自动化脚本来说完全够用。但它的天花板很低:所有状态都放在内存里,进程一重启就没了;没有并发能力,一次只能跑一个任务;出了错不知道该从哪里查起。这些问题会随着你对 Agent 的期待一起膨胀。
1.3 什么时候一次调用就是正确答案
我不想让这篇文章变成"人人都需要复杂架构"的怂恿文。反过来,我想先劝退一部分人。
如果满足以下条件,你可能根本不需要 Agent,一次或几次固定流程的 LLM 调用反而更合适:
- 任务的输入输出模式基本固定,不存在需要模型自主决定"下一步做什么"的场景
- 不需要调用外部工具,或者工具的调用顺序完全由代码决定,而不是由模型决定
- 单次调用的失败可以通过重试或规则兜底解决
- 没有跨会话、跨用户的状态需要维护
举个例子:一个把用户投诉内容分类并抽取关键字段的系统。写一个固定的 Prompt,输出 JSON,分类和抽取用正则校验就够用了。如果你非要对这个需求上 RAG、加记忆、加工具调用,结果就是系统响应慢三倍,失败率高两倍,每个月多烧一堆冤枉钱。
所以架构选型的第一个原则是:能用更简单的方案解决问题就不上复杂的。而复杂度的真实需求,是从"模型需要被赋予更多自主性"那一刻才开始的。
2. 中间形态的演进:Router、工具系统与状态管理
当你确定自己的场景确实需要模型具备一定的自主决策能力时,架构演进就开始分叉了。这时候你面临三个核心问题:多个任务链路如何分流、工具调用如何可靠地工作、跨步骤的状态如何保存。这三个问题解决到不同程度,就产生了不同的中间架构形态。
2.1 第一个分岔:从单调用到 Router
我见过最多的早期演进是加一个分流层,也就是 Router 模式。想象一下,你原来只有一个"通用问答"入口,现在业务方要求支持"查天气""算折扣""查物流"三个场景。你当然可以把三个场景的 Prompt 塞到一个大调用里让它自己分辨,但这样做的后果是三个场景的指令互相干扰,模型输出质量下降,而且改一个场景的 Prompt 可能会影响另外两个。
路由的思路是把"意图识别"这一步独立出来:
def route(request): intent = llm.classify(request.text, ["weather", "discount", "logistics"]) handler = handlers[intent] return handler(request)这个模式的优点是在不引入复杂编排的前提下,让系统具备了"多任务处理"能力。你可以认为它是最早的"Agent 决策"——模型做一个低成本的分类决策,然后把控制权交回代码。
实际做的时候有几个细节值得注意:
- 意图分类结果的置信度必须能被量化。一些框架支持让模型输出概率或理由,当置信度低于阈值时需要走兜底逻辑,比如转接人工、要求用户换一种说法。
- Router 本身要保持轻量,最好不要在分类这一步调用大参数模型做长推理。用小模型、短 Prompt、结构化输出,把延迟控在几百毫秒内。
- 意图列表要持续迭代。我们后来维护了一个"意图混淆清单",把历史上分错的问题积累下来,定期补充到 Prompt 里作为反例,分类准确率就是这么从 80% 慢慢磨到 96% 的。
2.2 工具调用:让模型真正"动手"的关键一跳
Router 只解决了任务分流,模型还是只会"说"不会"做"。真正让 Agent 变得有用的,是工具调用,业界通常叫Function Calling或Tool Use。
它的原理并不玄乎:你在请求里声明一批工具的模式(名称、参数、描述),模型在回应时输出一个结构化的调用意图,而不是直接输出自然语言。你的代码负责解析这个调用意图,执行对应的函数,把结果再塞回去让模型继续推理。
一个可靠的工具调用层,需要考虑的远不止"调通",还有这些工程细节:
参数校验。模型输出的 JSON 偶尔会带上多余的键、缺少必填字段、或者数字和字符串类型对不上。我们当时做了一个轻量校验层,基于 JSON Schema 做校验,不过就重试一次,重试还不过就把校验错误作为反馈喂回模型,让它自己修正。这样"模型手滑写错参数"的失败率最终稳定在千分之一以下。
超时与幂等。工具调用的耗时可长可短,有些接口还会超时。Agent 整体必须有明确的超时预算,比如"每个工具最多 10 秒,整轮 Agent 最多 5 分钟"。同时所有写操作最好设计成幂等的,带上请求 ID,否则一次超时后的重试可能造成两次扣款、两封通知短信。
工具描述的管理。给模型看的工具定义是有 Token 成本的,几十个工具的描述加起来可能超过两千 Token,既烧钱又容易让模型"眼花"。我们后来给工具打上了标签和优先级,根据意图分类结果只加载当前场景可能用到的工具子集,Token 成本立省三成。
2.3 状态管理:需要的不是存"历史对话",而是存"关键事实"
进入多轮工具调用的世界后,你就绕不开一个话题:状态到底怎么存。
很多人的第一反应是"把对话历史全存下来,每次请求都塞给模型"。这在两轮三轮的时候还能跑,到了二十轮就是一个灾难——上下文里塞满了"好的""我明白了""请稍等"这样的废话,真正重要的信息反而被稀释了。而且每次请求的 Token 消耗线性增长,延迟也是。
更合理的方式是维护一种状态摘要机制。具体来说,每一轮对话结束后,用模型生成一段话概括当前的关键事实:用户的目标是什么、已经获取了哪些数据、有哪些尚待确认的信息。下次请求时,把这段摘要 + 最近一两轮完整对话 + 当前工具结果拼在一起,而不是把全部历史都倒进去。
我们内部有个经验公式:如果摘要超过 500 Token,说明 Agent 在绕弯子,需要触发"澄清或收敛"的子流程。这算是一个比较实用的调优手法,让状态摘要本身成为牵引 Agent 收敛的信号。
再往下走,你还需要考虑状态怎么持久化。最简单的做法是放 Redis,以会话 ID 为键,保存整个上下文对象。再复杂一点,可以把关键事实结构化成字段存数据库,这样统计、检索、复现都能做。但这一档不推荐直接做,原因后面在 Harness 章节里一起讲。
2.4 流程控制:从 if-else 到状态机
当 Agent 要处理的子任务增加到一定数量,纯代码编排会变得非常痛苦。你可能遇到这种场景:
- 任务 A 有五种结果,每种结果诱导不同的下一步;
- 某一步失败需要重试,重试三次失败后要走人工兜底;
- 用户可以随时中断或改变目标。
如果用 if-else 硬写,最直接的问题是:你无法回答"当前整个 Agent 处在什么状态"这个问题。而这个问题恰恰是后面做监控、做恢复、做回放的基础。
状态机的引入是一个天然的解耦点。把 Agent 拆成几个明确的节点:收集需求→查询信息→生成方案→确认执行→反馈结果,每个节点内部是普通的业务逻辑,但节点之间的转移由状态定义,而不是由代码里的 goto。
这样做最大的好处是可预测。任何时刻你都能回答"当前 Agent 在哪个环节""下一步有哪几种可能",这对调试和向业务方解释系统行为极其重要。后面我们升级到完整 Harness 时,状态机的思路被直接迁移了过去,定义成 Harness 里的 Node 和 Edge,只是把原来的单进程实现换成了分布式的。
3. 完整 Harness Runtime:企业级 Agent 的运行时底座
现在聊正题。当你的 Agent 需要支撑的不只是几个 Demo 或内部脚本,而是一整套对外服务的系统时,前面那些手写拼装就开始不行了。你会碰到一批新的问题:同时几十个 Agent 在跑怎么调度?进程崩溃了上下文怎么恢复?线上出了问题怎么定位是哪一轮的哪个决策错了?用户投诉了你要怎么回溯当时的完整推理链路?
这些问题的答案,就是在 Agent 外面套一层运行时,也就是标题里的Harness Runtime。我的理解是,它的定位类似于 Nginx 之于 Web 服务,或者 K8s 之于容器:Agent 是业务逻辑,Harness 是承载它的环境。
3.1 任务调度与并发模型:别让 Agent 们互相踩踏
第一个要解决的问题是并发。你不可能让每个请求都启动一个裸进程,然后大家抢同一个 LLM API 的配额。
Harness 层需要包含一个任务调度器,它的职责有几个:
- 把不同请求的 Agent 实例分配到可用的执行资源上
- 控制对 LLM API 的总并发量,避免触发限流或超额扣费
- 设定每个任务的优先级。比如"用户实时交互"的任务应该比"后台数据清洗"的任务享有更高的执行优先级
- 支持任务的暂停、恢复和取消
调度策略上我们用的是带权重的队列 + 简单的工作池。每个 Agent 实例是一个可挂起的协程,模型调用和工具调用是它的两个 I/O 点。当时量化了一下,一个 8 核 16G 的节点,跑一个中等复杂度的 Agent(平均 5 步循环),大约能支撑 40 个并发实例。瓶颈几乎总是 LLM API 的响应时间,而不是 CPU 或内存。
在这个阶段强烈建议做限流熔断。大模型服务偶尔会抖动,一次几分钟的毛刺足以拖垮整个 Harness。我们在 Harness 里接入了一个简单的熔断器:当模型调用的错误率连续一分钟超过 5% 时,直接切到降级模式,比如只允许只读类 Agent 继续运行,写操作类 Agent 全部排队等待。
3.2 Context 生命周期管理:Token 是硬通货
我在 2.3 节提到状态摘要机制,在 Harness 里会被提升到一个更系统的层面,叫Context 生命周期管理。这个词听起来很重,但核心就是回答三个问题:
- Context 放在哪里?
- 什么时候压缩?
- 如何在多个环节间流转?
先看放在哪里。对于 Harness 这种多实例、可重启的系统,Context 必须持久化,不能只在内存里。我们用了数据库存基础事实字段 + 对象存储存会话快照的组合。这样即使处理节点宕机,也能快速拉起一个新实例,从最后一个快照继续跑,用户无感知。
再看压缩策略。一个 Agent 长跑起来,上下文膨胀几乎是必然的。我们设了三档水位线:
- 当上下文字数接近模型的窗口上限一半时,触发摘要压缩,把早期对话替换成摘要
- 当上下文仍然继续膨胀时,触发结构性压缩,把工具调用结果截断为"关键字段 + 摘要"
- 当用完模型最大窗口时,触发强制收敛,要求 Agent 必须在两轮内给出最终结论,否则强制结束进入人工处理
这三档策略写下来很简单,但实际调优时你会发现,摘要生成也是要调模型的,压缩本身就增加一轮或多轮调用,会增加延迟。这就需要在"省 Token"和"保信息完整"之间做权衡。我们最终的方案是:摘要用便宜的小模型生成,而且只在状态发生变化时才重新生成,对话连续不变的阶段不重复压缩。
3.3 可观测性:Trace、Eval 与 Replay 的三件套
普通后端系统的监控关注的是请求量、延迟、错误率。到了 Agent 这里,光有这些远远不够。你会遇到一个全新的痛点:一个响应用户请求的 Agent 进入了死循环,它每一步都成功调用了工具、拿到了结果,但最终回答是错的,你怎么定位?
这时候需要的是一个能还原"模型完整推理链路"的观测系统,关键点有三个:Trace、Eval、Replay。
Trace 是链路追踪的 Agent 版。普通链路追踪记录的是"哪个服务调用了哪个服务",Agent 的 Trace 需要额外记录"模型在每一步看到了什么上下文、选择了什么动作、工具返回了什么、置信度多少"。我们实现的时候,把每个 Agent 实例的每一步决策都写成一个结构化事件,链条从头串到尾。排查问题的时候,直接在界面上看这个链条,基本一眼就能看出是哪一步的推理出了偏差。
Eval 是回归测试集。Agent 系统最怕"跑着跑着行为漂移",因为模型升级、Prompt 微调都可能改变行为。我们把历史上有代表性的问题沉淀成 Eval 集,每次改动都跑一遍,输出对照结果。没有 Eval,后面的所有优化都是在裸奔。
Replay 是前两者的结合。线上某个请求出问题时,把它的完整 Trace 拿出来,原样重放一次,就能快速复现问题,甚至是带着修改后的 Prompt 重放,对比"修正前"和"修正后"的行为差异,从而验证改动是否有效。这三个能力合在一起,构成了 Agent 系统迭代的地基。
3.4 工具网关与安全边界:Agent 的手必须有笼子
早年间我见过一个"翻车"案例:某个内部聊天机器人接了一个"执行 Shell 命令"的工具,结果有同事在对话里输入了一段删除操作,机器人老老实实执行了。这个故事足以说明,凡是让 Agent 连接真实系统,就必须在工具层加控制。
在 Harness 架构里,我会把工具调用的安全能力划分为三层:
- 只读优先:绝大多数工具默认设计成只读的。查询类的工具开放,写操作类的工具必须有场景标记、审批标记。这个没得商量。
- 白名单与参数约束:工具的调用范围必须受限。比如"查数据库"这个工具,需要在 Harness 里配置允许访问的表清单、强制限制返回行数和超时时间。
- 人工审批闸口:高风险的写操作、涉及敏感数据的操作,不直接执行,而是进入人工审批队列。在审批通过之前,Agent 的任务停留在"等待"状态。这个机制一开始被吐槽拖慢效率,但后来业务方发现,它帮他们拦截了至少两个会造成生产事故的操作。
安全边界不是"要不要做"的问题,而是"从哪个版本开始做"的问题。我见过太多团队为了进度先把工具全放开,结果上线后第一个月就在处理事故。至少从第一天起,把工具的权限模型骨架搭起来,哪怕先写成配置文件,也不要裸奔。
4. 架构选型的决策框架:什么时候该上哪一档
把一个技术演进路径讲清楚之后,回到最开始的问题:架构到底怎么选?
我的答案归纳成一句话:用需求倒推架构,不要用架构倒推需求。这里的"倒推"不是空话,而是有一套具体的评估维度。下面这个表格是我在多个项目里反复校验之后沉淀下来的决策框架,你完全可以当成一张检查清单来用。
| 需求维度 | 单次/少量调用 | 中间形态(Router/状态机) | 完整 Harness Runtime |
|---|---|---|---|
| 任务级自主循环 | 不需要或极少 | 局部需要,流程基本固定 | 核心能力,流程可能复杂多变 |
| 工具调用量 | 不调用或固定调用 | 10 个以内,场景聚焦 | 数十个以上,需要动态按需加载 |
| 并发规模 | 几 QPS 的 Demo | 几十 QPS 量级 | 高并发、可水平扩展 |
| 状态持久化需求 | 无 | 简单会话缓存即可 | 需跨会话、跨节点持久化 |
| 可观测性要求 | 打点日志即可 | 需要结构化日志和链路追踪 | 需要 Trace、Eval、Replay 全套 |
| 安全与合规 | 无所谓 | 工具白名单 | 全生命周期管控,人和审批 |
| 团队基建能力 | 你能调 API 就行 | 有基本的后端工程能力 | 有专职的 Infra/平台团队 |
这个表格的用法很简单:把项目需求逐条往里面对号入座,落在左边多就选择简单方案,落在右边多就应该直接规划 Harness 或者购买现成的 Agent 平台。如果某个项目处于左二和右二之间,那就先做中间形态,留好演进接口,不要一上来就铺大摊子。
4.1 一个实际案例:为什么那个"客服 Agent"最终上了 Harness
说一个实际案例,可能更有体感。
某团队要做的是垂直行业的客服助手,用户会和它进行多轮对话、它会查企业内部的订单系统、售后规则,还要在特定条件下发起退款申请。一开始他们打算用一个开源编排框架做一个脚本跑起来,需求阶段各方都挺乐观。
后来业务方补了一条需求:"我们要能对每一次客服答复做原因追溯,合规部门要求至少保存六个月的操作记录。"就这么一句话,直接把架构往 Harness 方向推了。因为没有完整的 Trace 和 Replay,你根本没法回答"这个退款申请是模型根据哪条规则做出的决策"这种问题。最后他们上了规范化的运行时方案,用完整的 Trace 链条做审计,才满足了合规审查的要求。
这个例子想说的是:业务需求里一个看似不起眼的要求,可能直接把架构复杂度拉高一个量级。所以一开始必须把"审计、容灾、恢复"这类需求问清楚,而不是只问"你要实现什么功能"。
4.2 现阶段这些架构的代价分别是什么
光说哪些维度对应哪档架构还不够,得把代价也说清楚,不然容易做出一厢情愿的决定。
单次/少量调用方案的代价在于能力天花板低。你想升级的时候,几乎等于重写,因为前面没有为"循环"和"状态"留任何结构。中间形态的代价在于它是个"中间品":Router 和状态机解决的是流程和状态问题,但并发调度、可观测性、安全管控这些和业务几乎无关的横切能力要自己一点点补,每补一块都挺费劲。完整 Harness Runtime 的代价则非常直白——它是一笔不小的基建投入。光是调度器、Context 管理、Trace 系统和工具网关这几个模块,一个靠谱的团队至少要投入两到三个月才能达到可用状态,后续还需要人手持续维护。
所以选型有一个非常朴素的"成本账":如果你的 Agent 要承载真实用户量、要成为公司业务的一部分,那 Harness 的这些能力迟早要补齐,早做比晚做便宜。如果只是验证想法,那 DIY 就好,别背基建包袱。
5. 落地实践中的经验:从原型到生产的坑与反思
最后聊点软性的东西——实际操作中的感受和踩过的坑。因为选型文档写得再漂亮,落地的时候总有意外。
5.1 工具调用的"最后一公里"问题
第一个反复踩坑的点是工具调用的可靠性。模型输出的结构化参数看着像模像样,一执行就报错。最典型的是参数类型不对,比如接口要的是整数,模型给的是字符串。你觉得这是小事,但 Agent 在无人值守的情况下会自动重试,然后继续失败,最后整个任务超时。我们的对策是加了一个"宽容解析层":先做一次类型强制转换,再校验必填字段,最后用一个"最小可执行参数集"的兜底策略。说白了,就是别让模型的一个小失误拖垮整个任务。
另一个经验是工具的结果要"结构化"再送回模型。很多初版设计直接把工具返回的原始 JSON 塞进上下文,结果模型被一堆无关字段带偏了。我们后来统一在工具返回值上加了一层"summary 字段",由工具代码自己生成一段面向模型的重点摘要,实践下来推理准确率有肉眼可见的提升。
5.2 上下文膨胀的隐性灾难
第二个坑是上下文膨胀的隐性灾难。它不像报错那么明显,而是渐进地劣化你的系统:响应越来越慢,Token 账单越来越高,模型越来越容易被无关信息干扰。而且它有很强的欺骗性——单看每个请求没觉得哪里不对,直到月底账单出来才发现成本翻了好几倍。
解决办法就是我前面说的三档压缩机制。但这里有个新的坑:摘要生成本身也会成为成本。如果每一步都触发摘要生成,压缩省下的 Token 又被摘要生成花掉了。我们最终的做法是:摘要只对"跨工具调用的关键中间结果"生成,对于纯用户对话的记录,直接用滑窗截断即可,不需要每个细节都保存。
5.3 "上 Harness"不是终点,只是起点
关于架构,最后说一点个人的反思。
很多人把"上了 Harness"当成项目的里程碑,以为跑起来就万事大吉。但真实情况是,Harness 解决了"运行环境"的问题,却把压力转移到另一个地方:你的 Eval 集够不够好,你的 Trace 分析能不能形成闭环,你的人机协作流程是不是顺畅。这些才是决定 Agent 长期质量的东西。
我见过一个团队,Harness 搭建得很规范,调度、权限、链路监控都有模有样,但 Eval 集只有五十多个用例,而且全是 Happy Path。结果模型一升级,线上表现直接大变样,他们愣是没发现。反观另一个团队,基础设施朴素得能用就行,但每天坚持把线上新产生的用户反馈沉淀成用例,三个月后系统的行为稳定性反而高出一截。
所以我个人的实际操作体会是:架构选型只是画了一个地基的轮廓,地基里每一块砖的质量,取决于你有没有一套持续迭代、持续验证的机制。如果你的 Agent 还处于早期,从最轻的方案起步没有问题,但一定要把"状态管理"和"决策可回溯"这两件事放在心上,哪怕只用最朴素的日志方式先实现。等业务量证明你需要一个更重的底座时,你会发现之前这些沉淀全都没有白费。