首页 / 资讯中心 / 文章详情

AI Agent 架构选型:从 LLM 调用到 Harness Runtime

AI Agent 架构选型:从 LLM 调用到 Harness Runtime ★ FEATURED ARTICLE
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, contextNone): 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 和完整观测体系中间改过三轮架构。回头看不套这五层结构第三轮时就撑不住了。当然不是所有系统都要走到第五层很多人可能只需要前两层加上一个简单的任务编排。技术选型最贵的从来不是代码是选错方向后返工的时间和团队的耐心。如果你正在做一个会被多个团队长期使用的智能助理系统我建议直接参考这套分层如果你只是做一个内部小工具让模型帮忙改改文案、翻译几句那我的建议是——今天就到这里别往下读了。
阅读完成 · 觉得有帮助?
咨询建站