AI Agent 架构怎么选?这个问题我最近一年被问得太多次了。大多数人都是一路踩过来:先调通一次 LLM 调用,然后觉得“这不就是个 API 吗”,再做几个工具函数,拼一个循环,最后发现系统开始不可控——上下文越滚越大、工具调用偶尔失败、Agent 原地打转、一次任务几十秒甚至几分钟,完全不知道它在干什么。到了这个阶段,你其实不是在写 Agent,而是在写一个运行时(Runtime)。这次我把自己从一次裸的 LLM 调用,逐步设计到 Harness Runtime 的完整选型过程整理成文,覆盖每一层该做什么、为什么这么做,以及我在某个内部项目里的实测结论。内容偏向工程落地,想直接抄作业的可以跟着结构走,想理解原理的也能从中看到取舍依据。
先说清楚一个容易被忽略的事实:单次 LLM 调用和 Agent 系统之间的差距,不是“多写几个 if 和 while”,而是“从无状态函数变成了有状态、有工具、有生命周期的执行环境”。很多团队在第一步就走错了——他们把 Agent 框架当成一个可以随时换的剧本模板,结果业务一变,整个交互链路全崩。所以下面这套分层方案不是唯一的答案,却是目前我验证下来最不容易翻车的一种。
1. 单次调用、Agent 与 Harness Runtime 的分界线在哪
1.1 一次 LLM 调用的本质是“一问一答”
先看最基础的一层。如果你只是给自己的工具加一个“总结解释”功能,那么一次 LLM 调用就够了。你需要做的无非是拼一个 Prompt,丢给模型,拿到字符串,然后展示给用户。这个链路里没有“决策”、没有“工具”,也没有“记忆”,模型不知道自己上一轮说过什么——除非你手动把历史消息塞进上下文。
麻烦的是,很多人把这一层当成了整个 Agent 架构的全部。于是他们做的所谓 Agent,实际上是一个“把工具结果塞进 Prompt 的聊天机器人”。它也能工作,但本质上是一次次重复的“问-答”,系统本身不包含“规划”和“状态”的概念。只要业务场景稍微复杂,比如需要多步工具调用、失败重试、分支处理,这层所谓 Agent 就顶不住了。
我自己的判断标准是:如果你的任务逻辑是确定的、线性的,不要上 Agent;如果任务逻辑不确定,需要模型自己决定下一步,才需要引入循环和运行时。这里的“不确定”是核心关键词,后文所有架构设计都围绕它展开。
1.2 Agent 需要一层 Runtime,不是需要一套“框架”
Agent 和普通程序最大的区别在于:模型每一步输出都可能改变后续路径。工具执行结果可能成功、可能失败、可能返回超长文本,模型也可能跑偏。如果没有一套东西来约束和托管这个过程,你只能把控制逻辑全部堆在业务代码里,最后变成一坨“胶水代码”。
这也是为什么我坚持引入 Harness Runtime 而不是直接在上面写业务循环。所谓 Harness,可以理解成一个“壳”,它负责管理 Agent 从启动到结束的全过程,包括上下文、步骤计数、资源限制、并发控制、持久化和可观测性。业务代码只需要关心自己的领域逻辑,运行时统一处理“模型输出-工具调用-环境反馈”这个循环。
做个类比:写一个 LLM 调用就像写一个单函数;写一个完整 Agent 就像开发一个后台任务系统。后者需要进程管理、超时控制、日志、任务状态机。Harness Runtime 就是那套后台系统,Agent 只是它调度的任务。
1.3 用一张决策表确定你的架构边界
我每次给团队做技术评审,都会先问三个问题:你的任务链路是固定的吗?允许模型自由决定工具顺序吗?需要多人/多任务共享状态吗?答案直接决定架构复杂度。
| 场景形态 | 你需要实现的层 | 是否需要 Harness Runtime |
|---|---|---|
| 单轮问答、固定 Prompt 模板 | 模型调用层 | 否 |
| 多轮对话,无工具,上下文拼接 | 模型调用层 + 会话管理 | 否,但要写消息存储 |
| 固定流程调用多个工具,顺序写死 | 模型调用层 + 工具函数封装 | 否,用词条配置调度即可 |
| 模型自由决定调用哪些工具、可能多轮尝试 | 模型调用层 + 工具协议 + Agent 循环 | 是,至少一个轻量 Runner |
| 多 Agent 协作、长期运行、需要暂停/恢复/审计 | 完整五层,Harness 托管所有 Run 实例 | 必须 |
如果你正处于第三行到第四行之间,建议直接参考下面的五层结构。如果你已经在第五行,那光写业务代码已经不够了,需要一个真正的运行平台,也就是 Harness。
2. 选型前先想清楚的四件事
2.1 你的任务需要“规划”还是只需要“执行”
大部分 Agent 失败的根源,是把“规划”放错了位置。一类任务是规划密集型,比如“帮我分析一批合同,找出风险条款,再生成一份摘要”,模型需要看情况决定先读哪些文件、用什么工具、什么时候停止。另一类是执行密集型,比如“每天定时把 A 表数据同步到 B 表,失败重试 3 次”,这类任务根本不需要模型参与决策,只需要一个定时任务加工具调用。
我见过最离谱的设计,是把一个定时同步任务用 Agent 框架重写了一遍,结果模型偶尔会把目标表名改错,整条链路反而变得更脆弱。所以在架构选型之前,先把任务拆一遍:哪些步骤必须由模型决定,哪些步骤可以让模型决定但由代码兜底。能写死就不要让模型自由发挥,规划是能力也是风险。
2.2 你能接受多大的错误容忍度
自由调用工具意味着每一步都可能出错。一个链路里如果有 5 个工具调用,每个工具的成功率是 95%,整条链路成功的概率就已经降到 77%。这不是模型“笨不笨”的问题,而是工具协议、参数校验、重试机制、上下文反馈共同决定的结果。
选型时要把错误处理当成一等公民。工具抛异常后怎么办?返回错误信息给模型让它自愈,还是直接终止任务?参数校验失败是重试还是换策略?这些设计必须在 Harness 层统一处理,而不是散落在各个工具内部。否则一旦出错,你连“是工具坏了还是模型选错了”都分不清。
2.3 并发和资源限制决定运行时上限
很多人从单次调用直接跳到完整 Agent,却忘了考虑:Agent 比单次 LLM 调用要昂贵得多。一次任务会发起多次模型请求,可能还要等待外部接口响应,占用更长的执行时间和内存。如果你要支持并发用户,每个用户一个 Run,必须要有资源池、队列、超时和并发限制。
我的经验是:先定好单次任务的最大步数、最大 Token 消耗、最大耗时,再谈并发。这些硬限制就是 Harness 的边界。没有边界的 Agent 系统上线后,唯一结果就是账单失控和环境资源被打爆。
2.4 后续你打算升级到什么复杂度
最后一点是扩展路径。选型时不要只考虑当前需求,要想一个月后你可能要增加什么:更多工具?多轮人工审批?Agent 和 Agent 之间的消息传递?如果一开始就把模型调用、工具调用、状态管理混在一个类里,后面每加一个能力都是重构。
这也是我坚持五层分离的原因。模型网关、工具协议、Agent 循环、状态存储、Harness Runtime 各管一摊,互相之间只通过接口通信。哪一层要换,不影响其他层。这套结构在项目 X 里从单 Agent 扩展到多 Agent 共享工具注册表时,几乎没有改动底层接口。
3. 从模型网关到 Harness Runtime 的五层结构
3.1 第一层:模型网关与统一消息结构
第一层要解决的第一个问题是“模型不等价”。不同模型的 API 参数、返回结构、Token 计算方法都不同,甚至同一个模型不同版本的行为也可能变。如果你把模型 SDK 直接塞进业务代码,后续换模型、做 A/B 测试、加 fallback,都会非常痛苦。
我先定义一套统一的消息中间格式:角色(user/assistant/system/tool)、内容、附加字段(工具调用列表、工具返回标记)。所有模型接入层负责把自家消息格式转换成这套统一格式。例如:
@dataclass class ChatMessage: role: str # "system" | "user" | "assistant" | "tool" content: str tool_calls: list[dict] | None = None tool_call_id: str | None = None统一消息格式的好处是:后续 Agent 循环、上下文压缩、日志审计全部面向这套数据类操作,与具体供应商无关。比如日志里可以直接打印 tool_call_id 和 tool_calls,排查时一眼就能看到模型调用的是哪个工具、传了什么参数。如果不统一,每个模型返回的字段名不同,排查成本会指数级上升。
网关层还必须包含模型配置管理:模型名、温度、最大输出 Token、超时、重试次数,甚至多模型 fallback 策略。我习惯把这些配置做成结构化配置,而不是写死在代码里。业务层只需要指定“用哪个配置”,不关心背后是哪个模型。
3.2 第二层:工具协议与安全边界
Agent 的能力上限取决于工具层。很多人一开始只用 Python 函数当工具,函数内部直接操作数据库、文件、外部 API。这在 Demo 阶段没问题,一旦上生产,你会发现工具层的问题比模型还多。
工具协议要统一输入输出格式。我常用的工具描述结构如下:
{ "name": "query_order", "description": "根据订单号查询订单状态", "parameters": { "type": "object", "properties": { "order_id": {"type": "string"} }, "required": ["order_id"] } }这看起来很像 OpenAI Function Calling 的结构,但我要强调一点:哪怕你不用 Function Calling,也应该按这个规格存工具 schema。因为模型的工具选择依赖的是清晰的描述和参数定义,而这个 schema 可以同时用于参数注入、运行时校验、模型提示词生成。一个 schema 多处复用,比每个工具手写 Prompt 模板靠谱得多。
工具执行的边界必须由代码控制:超时时间、权限、资源配额、是否允许副作用。工具返回的结果建议做截断,尤其是数据库查询或网页抓取,一次返回几万字会让模型直接“迷失”。我实践中的做法是:默认工具结果最多 4000 字符,超出部分自动截断并加一行提示“结果过长,已截断,可通过 XX 参数进一步筛选”。
3.3 第三层:Agent 循环与规划策略
Agent 循环是整个系统的大脑。常见模式有 ReAct(推理 + 行动)和 Plan-and-Execute(先规划再执行)。我不认为这两个模式孰优孰劣,关键看任务类型和成本承受能力。
ReAct 适合需要边做边调整的任务,模型每一步都根据上一步结果决定下一步。实现简单,但 Token 消耗大,而且容易在小决策上反复横跳。Plan-and-Execute 适合任务路径相对清晰但步骤较多的场景,先让模型生成一个计划,再用代码逐步执行。计划模式的代价是“计划可能一开始就错”,但好处是省 Token、可读性强,适合需要人工审核中间步骤的场景。
我在项目 X 里做了可插拔策略:默认用 ReAct,对特定任务类型切换到 Plan-and-Execute。这个切换放在 Harness 的配置里,不写死在代码中。核心循环伪代码大致如下:
class AgentRunner: def run(self, task, context=None): while not self._should_stop(context): step = context.current_step message = self.model.complete(context.messages) if message.tool_calls: tool_results = self.execute_tools(message.tool_calls) context.add_tool_results(tool_results) else: context.finish(message.content) return context.final_answer context.step += 1 raise AgentTimeoutError(context)这段伪代码省略了很多细节,比如上下文压缩、异常处理、事件发布,但展示了最核心的循环形态。你需要特别注意的是_should_stop方法,它不仅是“达到最大步数”,还包括“用户取消”“预算耗尽”“连续多个相同错误”等终止条件。没有这些终止条件,Agent 可能陷入死循环或无限调工具。
3.4 第四层:会话状态与记忆管理
状态管理是 Agent 架构里最容易被低估的一层。模型的上下文窗口是有限的,但业务对话可以无限长。为了让模型持续理解任务,你需要把“模型上下文”和“业务记忆”分开管理。
模型上下文 = 当前必须传给模型的消息窗口。业务记忆 = 可以做摘要、压缩、存数据库的长期信息。我每次进入新步骤前,会检查当前消息序列的长度。超过阈值就触发压缩:把早前的对话交给模型生成摘要,然后把摘要作为一条 system 消息塞回上下文,替代被压缩的原始消息。
事件式消息存储也很重要。不要把上下文只存在内存里,每次模型调用后,都把消息写入事件存储,标记顺序、Token 数、工具调用信息。这样一边可以支持暂停/恢复,一边可以补全审计日志。项目 X 中用了一张简单的消息事件表:run_id、sequence、role、content、tokens、created_at。任何一次出问题,都能根据 run_id 回放整个 Agent 轨迹。
3.5 第五层:Harness Runtime 与生命周期管理
最后一层就是标题里的 Harness Runtime。它负责托管所有 Run 实例。所谓 Run,就是一次完整的 Agent 任务执行过程。我把 Run 设计成明确的状态机:Pending、Running、WaitingTool、Paused、Completed、Failed、Cancelled。每一个 Run 都持有独立的上下文、配置、资源配额和事件流。
在这个基础上,Runner 还承担几个职责:并发控制、任务队列、持久化、可观测性。并发控制很简单,通过信号量限制同时运行的 Run 数量,超出部分排队;持久化则是把每个 Run 的关键事件和上下文快照写入存储,重启后可以恢复。
Harness 的接口大概这样:
class Harness: def submit(self, task: Task) -> RunId: ... def cancel(self, run_id: RunId) -> None: ... def pause(self, run_id: RunId) -> None: ... def resume(self, run_id: RunId) -> RunId: ... def wait(self, run_id: RunId, timeout: float) -> RunResult: ...没有这一层,所有 Agent 任务都只能“在浏览器里开着那个页面等它跑完”。一旦进程重启、服务发布、用户刷新,任务就丢了。上了 Harness 之后,至少你可以对一次执行过程做暂停、恢复、取消和重放,对运维来讲完全是两个维度。
4. 核心模块实现:循环终止、工具控制与结构化输出
4.1 循环终止条件要多写,不要把命运交给模型
模型在自由工具调用场景下经常陷入无意义重复:同一个失败的工具调用试三次,或者在没有新信息的情况下反复修改同一个参数。我的做法是配置三组终止条件:步骤上限、连续失败上限、预算上限。
步骤上限最简单,比如最多 20 步。连续失败上限更实用,我规定如果模型连续 3 次调用同一个工具且返回结果都是同一类错误,直接在上下文中注入一条强提示:“该工具连续失败,请换一种方式,或停止尝试并告诉你遇到的问题。”预算上限则是累计 Token 或累计费用,超过就强制停止,并把已收集的信息整理成报告返回给用户。
这三个条件缺一不可。只配步骤上限会遇到“20 步都在做同一个无效尝试”;只配失败上限会遇到“每步换一种失败方式,无限失败下去”;不配预算上限则会遇到“因为上下文越滚越大,单次调用成本越来越高,最后账单爆炸”。
4.2 工具调用必须做运行时校验,不能全信模型输出
模型生成的工具参数偶尔是合法 JSON,但字段值可能根本不存在。项目 X 里我遇到过模型把日期参数传成“明天”,把 ID 传成“最近一个订单”,把排序字段传成不存在列名的场景。因此工具调用不能直接透传给业务函数,必须有一层校验。
校验分两层。第一层是结构校验,用工具 schema 里的 JSON Schema 做参数类型和必填项检查。第二层是业务校验,比如订单号格式、日期范围、枚举值有效性。结构校验失败时,把校验错误信息当成工具返回内容送回给模型,让它修正参数。业务校验失败时,也要通过工具返回消息说明“为什么失败”,而不是让模型瞎猜。
工具执行超时也要单独处理。我建议把每个工具封装成独立执行单元,设置默认超时(比如 30 秒)。超时后返回一个标准的 tool timeout 错误消息给模型,模型可以选择缩小查询范围或换工具,而不是让整个 Run 卡死。
4.3 结构化输出别只靠 JSON 解析,要加自愈机制
Agent 最终需要给用户一个结构化结果,比如“报告对象是 JSON,包含结论、风险点、建议操作”。很多实现直接让模型输出 JSON,然后json.loads解析。模型输出偶尔会带 Markdown 代码块标记、尾逗号、注释,一次解析失败整个任务就崩了。
我做了两件事。第一,输出格式示例。在系统提示词里给出一个“成功的 JSON 示例”和“失败的 JSON 示例(带错误说明)”,让模型模仿。第二,解析失败时自动进入修复循环:把解析错误消息塞回上下文,让模型重新生成,最多重试 2 次。如果仍失败,返回部分内容加解析失败标记,而不是硬报错。这在生产中非常有用,因为 Agent 的最终用户更关心“拿到了多少有效结果”,而不是“这次任务因为解析失败白跑了”。
4.4 流式事件是 Harness 的第一个实感功能
一旦 Agent 跑起来,用户最直观的感受是“它怎么还在转”。如果你只在最后返回一个完整答案,中间过程完全黑盒,用户体验会非常差。所以 Harness 应该把执行过程发布成事件流,前端可以实时看到当前步骤、正在调用的工具、工具返回摘要、剩余步数。
我定义的事件类型很朴素:step_started、tool_started、tool_completed、agent_message、run_failed、run_finished。每一个事件都带 run_id、sequence、timestamp、payload。事件流不只是给前端用的,日志审计和多 Agent 协作编排也可以订阅同一套事件。这也是我刻意在 Harness 里内置事件总线的原因,而不是让业务代码自己打日志。
这里有个细节:事件内容不要包含完整工具结果,否则事件存储会爆炸。我一般只发布工具返回的前 200 字符摘要,完整结果只在内存里传递,最后统一写入上下文存储。
5. 生产环境常见问题与排查实录
5.1 Agent 原地打转:绝大多数是工具反馈信息不够
有一次项目 X 里的 Agent 反复调用同一个查询工具,每次都返回“未找到数据”。日志显示模型没有换策略,而是不断微调参数重试同一操作。后来我看了工具返回内容才发现,错误信息里只写了“未找到”,没有告诉模型可能的替代路径。加上一行引导提示后——“未找到当前数据,建议检查过滤条件,或尝试查询最近 7 天数据”——问题立刻消失。
排查这类问题时,先看事件流里的tool_completed摘要。如果连续多次tool_completed的内容结构一样,而step还在增长,基本可以断定问题出在工具返回信息对模型决策的引导不足,而不是模型“笨”。这条经验至少帮我省掉了一半的排查时间。
5.2 上下文膨胀导致成本线性爆炸
另一个高频问题是:上下文越滚越大,后面每一步的 Token 消耗都以先前全部历史为基数,成本飞涨。如果你发现同一个 Run 后面几步的调用耗时显著变长,多半是上下文过大导致的。
解决办法就是前面提到的上下文压缩。我设置了 3 个档位:上下文超过窗口 60% 时先做历史消息裁剪,只保留最近 N 条完整消息;超过 80% 时做摘要压缩;超过 95% 时强制让模型输出一个阶段性汇总,然后清空历史进入“继续模式”。实测下来,裁剪历史比直接压缩摘要更稳——摘要压缩可能丢掉关键细节,导致模型忘记前面的决策依据。
5.3 工具返回超长文本模型更容易“迷路”
模型对超长工具结果的注意力明显分散。一次数据库查询返回 100 行数据后,模型只顾着看前面几行,后面关键信息被忽略,导致最终答案不完整。
我的做法是:工具返回的时候直接生成一个数据摘要,之后再附加原始数据的精简行。例如查订单列表的时候,工具返回 “共 37 条订单,总额 120,540 元,最近一笔是订单 X,金额 Y”,而不是把 37 条订单全部铺开。只有当模型明确要求看明细时,才通过另一个工具传入分页参数拿原始数据。这个模式对几乎所有“查询类工具”都适用。
5.4 并发场景下的偶发失败与重试策略
Agent 一次任务可能调用外部接口多次。外部接口的偶发 5xx、超时、限流,都会让 Agent 中断。之前我直接在工具内部做 3 次重试,结果限流场景下重试反而加剧了限流。
后来我把重试策略移到 Harness 层,并且区分“可重试错误”和“不可重试错误”。HTTP 429、500、503 属于可重试,带指数退避;参数错误、认证失败、业务规则错误属于不可重试,直接返回给模型让它换方案。同时,在重试请求里带一个请求 ID,方便外部系统排查。这一套下来,Agent 的外部接口调用成功率从 92% 提到了 99% 以上。
5.5 可观测性的三个埋点:Trace、指标、成本
最后说说可观测性。没有它,Agent 生产环境就像个黑匣子。我维护三套数据:链路 Trace、核心指标、成本统计。
链路 Trace 记录一次 Run 里所有事件的父子关系,模型调用、工具调用、上下文压缩都作为独立 span。核心指标包括:每轮工具调用数和成功率、上下文 Token 趋势、单步耗时、终止原因分布。成本统计则按模型、工具、任务类型三个维度聚合 Token 消耗和费用。
我见过太多团队上线 Agent 后,只会在出问题时“看日志”,但日志里没有上下文压缩记录、没有工具返回摘要、没有成本归因,最后只能靠猜。如果你不想把 Agent 运维变成玄学,这三套数据一定要在 Harness 层就埋好。
6. 我最后想说的选型心得
如果现在我带团队,第一课永远是:先别急着选 Agent 框架,先把你需要跑的任务列清楚。任务链路固定,就老老实实写工作流;任务链路不固定,再引入 Agent 循环;需要托管一堆 Agent 并行跑,才值得上 Harness Runtime。架构是长在场景里的,不是为了炫技存在的。
第二课是:每一步都要有“为什么”。统一消息格式是为了换模型不被绑架;工具协议 of schema 是为了模型调用不出界;Harness 生命周期是为了任务可暂停可恢复。你在选型时每加一层复杂度,都要能说清楚这一层解决了哪个具体问题。说不清楚的那一层,就是过度设计。
项目 X 从最初十几行 LLM 调用,到现在包含模型网关、工具注册表、Agent 循环、会话压缩、Harness Runtime 和完整观测体系,中间改过三轮架构。回头看不套这五层结构,第三轮时就撑不住了。当然,不是所有系统都要走到第五层,很多人可能只需要前两层加上一个简单的任务编排。技术选型最贵的从来不是代码,是选错方向后返工的时间和团队的耐心。
如果你正在做一个会被多个团队长期使用的智能助理系统,我建议直接参考这套分层;如果你只是做一个内部小工具,让模型帮忙改改文案、翻译几句,那我的建议是——今天就到这里,别往下读了。