从单次LLM调用到Agent:Harness Runtime架构选型与实战避坑指南 1. 从一次裸调用说起为什么单次 LLM 调用撑不起一个 Agent很多人第一次接触 Agent脑子里想的其实很简单不就是把用户输入拼进 prompt丢给大模型拿到回复再返回给用户吗这个理解在 Demo 阶段完全够用但只要你想让它真正干活——比如读文件、查数据库、调接口、多轮修正——你会发现单次调用这条路走不了几步就崩了。我最早做的一个小工具就是这种裸调用结构用户问一句我拼一段 prompt调一次模型把结果打印出来。前两周用着挺爽第三周开始出问题。用户问帮我看看这个报错日志里哪一行是根因模型说我需要看到日志内容但我的代码里根本没有把日志塞进去的机制因为上一轮对话里用户只是提了一嘴日志在某个路径下。模型没有工具去读文件也没有记忆去记住那个路径更没有循环去先读再判断再回答。这就是单次调用的天花板它只能做一次映射做不了多步决策。Agent 的本质区别在于它把一次映射变成了一个循环。这个循环里至少要有四样东西模型推理、工具执行、状态记忆、循环控制。少了任何一样它就不是 Agent只是一个带格式的文本生成器。而把这四样东西组织起来的那层代码就是标题里说的 Harness Runtime——你可以把它理解成 Agent 的操作系统模型是 CPU工具是外设Runtime 负责调度、传参、兜底和终止。所以这篇文章想解决的问题很具体当你决定从一次 LLM 调用升级到一个完整 Agent 时架构该怎么选、每一层要做什么、哪些坑必须提前避开。我会按从简到繁的顺序把几种主流架构拆开讲每一种都告诉你它适合什么场景、代价是什么、我实际踩过哪些坑。不管你是刚写完第一个 Demo 想往上加功能还是已经在维护一个多工具 Agent 想重构应该都能找到能直接抄的部分。2. Agent 架构的四层拆解与选型逻辑2.1 先搞清楚 Agent 到底由哪几层组成在选架构之前得先把 Agent 的组成拆清楚。我习惯把它分成四层从下往上依次是模型层负责推理和决策输出下一步做什么。它可以是单模型也可以是多模型分工一个规划、一个执行、一个校验。工具层模型能调用的所有外部能力包括读文件、发请求、查库、执行代码等。工具的定义质量直接决定 Agent 的上限。状态层保存对话历史、中间结果、工具返回值、任务进度。没有状态层Agent 就是金鱼记忆每轮都从零开始。控制层Harness Runtime决定循环怎么转、什么时候停、出错怎么办、多个工具怎么串。这是最容易被低估、也最容易写乱的一层。这四层不是必须物理分离小项目里它们可能全挤在一个文件里。但逻辑上必须清晰否则你会在加第三个工具的时候发现代码已经没法维护了。我见过太多项目工具调用逻辑和 prompt 拼接逻辑混在一起改一个工具要动五处代码最后没人敢动。选架构的本质就是决定这四层怎么组织、耦合到什么程度。下面几种方案基本覆盖了从个人项目到生产系统的常见选择。2.2 四种主流架构的适用场景对比我把常见的 Agent 架构归成四类按复杂度递增排列。先给一张对比表后面逐个展开。架构类型核心特征适合场景主要代价单轮工具调用一次推理一次工具执行简单查询、单步操作无法处理多步任务ReAct 循环推理-行动-观察循环中等复杂度、工具少于10个容易死循环、token 消耗大规划-执行分离先出计划再逐步执行长任务、步骤明确计划僵化、难应对变化多 Agent 协作多个角色分工协作复杂任务、需要多视角通信开销大、调试困难这张表不是让你照着选最复杂的而是让你知道每往上走一层你换来的是什么、付出的是什么。我见过有人上来就搭多 Agent 系统结果任务简单到单轮调用就能解决白白增加了三倍调试时间。架构选型的第一原则是用能解决问题的最简架构。2.3 选型时最该问自己的三个问题在动手之前我建议先回答三个问题它们基本能帮你锁定架构任务需要几步如果一步就能完成比如把这段话翻译成英文单轮调用足够。如果需要先查再算再写那就得上循环。工具会不会互相依赖如果工具之间没有依赖比如查天气和查汇率互不相关ReAct 循环就够。如果后一步依赖前一步的输出比如先读文件再根据内容改文件规划-执行分离更稳。失败代价高不高如果一次错误调用会写坏数据、发错消息那必须加校验层甚至上多 Agent 互相检查。如果只是查个信息错了重试就行。这三个问题我在每个项目启动前都会过一遍实测下来能省掉大量返工。很多人架构选错不是技术不行是没想清楚任务本身长什么样。3. 核心细节解析Harness Runtime 到底要做什么3.1 循环控制什么时候继续、什么时候停Harness Runtime 最核心的职责就是控制循环。一个典型的 Agent 循环长这样while not done: response model.invoke(messages, tools) if response.has_tool_call: result execute_tool(response.tool_call) messages.append(result) else: done True final_answer response.content看着简单但魔鬼在细节里。第一个坑是终止条件。如果模型一直不返回最终答案一直调工具你的循环就会无限转下去。我踩过的坑是模型在某个工具报错后反复重试同一个工具每次都失败每次都重试token 烧了一大把。后来我加了三道保险最大轮次限制硬性限制循环次数比如 15 轮超过就强制终止并返回当前状态。重复调用检测如果连续三次调用同一个工具且参数相同直接中断提示模型换策略。无进展检测如果连续两轮的状态没有实质变化比如工具返回内容一样判定为卡死。这三道保险不是可选项是必选项。任何上生产的 Agent 都必须有否则你迟早会遇到一个烧光预算的循环。第二个坑是工具调用的错误处理。工具执行失败时你不能直接把异常抛出去让整个 Agent 崩掉也不能假装成功。正确做法是把错误信息作为工具返回值塞回消息历史让模型看到这个工具失败了原因是 X它才有机会换个方式。我一般会把错误分成三类处理可重试错误网络超时自动重试 2 次仍失败则返回错误给模型。参数错误模型传了不存在的字段直接把校验信息返回给模型让它修正。致命错误权限不足、资源不存在返回明确错误并建议模型换工具或告知用户。3.2 状态管理消息历史怎么存、存多少状态层最容易被忽视但它决定了 Agent 能不能做长任务。核心问题是消息历史越攒越长token 迟早爆掉。我见过一个 Agent 跑到第 20 轮时光历史消息就占了 8 万 token模型开始胡言乱语。解决思路有几种我按推荐程度排滑动窗口 摘要保留最近 N 轮完整消息更早的用模型压缩成一段摘要。这是最实用的方案我一般保留最近 6 到 8 轮完整对话更早的压缩成 200 字以内的摘要。关键信息提取把工具返回的大块内容比如整个文件提取成关键字段存起来原始内容丢弃。比如读文件后只保留文件路径、行数、关键行内容而不是整个文件文本。外部存储把完整历史存到数据库或文件只在上下文里放引用 ID。适合超长任务但增加了检索复杂度。提示状态压缩本身也要调模型会额外消耗 token 和时间。我的经验是只有当历史超过阈值比如 10 轮或 2 万 token时才触发压缩不要每轮都压。3.3 工具定义模型能不能用好全看这里工具层是 Agent 能力的边界。模型再聪明工具定义得烂它也调不对。我总结了几条工具定义的硬规矩描述要写什么时候用不只是是什么。比如一个查订单的工具描述里要写当用户询问订单状态、物流进度、预计送达时间时使用而不是只写查询订单。参数要少而明确。一个工具超过 5 个参数模型出错率会明显上升。参数名要自解释别用arg1、param2这种。返回值要结构化。返回 JSON 比返回一段自然语言好模型解析更稳。同时要控制返回长度别把整个数据库 dump 回去。工具数量要克制。我实测下来单次给模型超过 15 个工具选择准确率会下降。如果工具很多考虑分组或分层暴露。这里有个反直觉的点工具不是越多越好。我早期恨不得把所有能力都做成工具结果模型经常选错。后来我把工具砍到 8 个核心的准确率反而上去了。工具多了模型的选择空间变大决策难度指数级上升。3.4 提示词工程系统提示词是 Agent 的宪法系统提示词在 Agent 里的作用比在普通对话里重要得多。它不只是设定人设更是规定行为边界和决策规则。我写系统提示词一般包含这几块角色和职责这个 Agent 是干什么的不干什么。工具使用规则什么情况下用哪个工具什么情况下不用。输出格式要求最终答案长什么样中间步骤要不要暴露。失败处理策略工具失败时怎么办不确定时怎么办。禁止事项明确列出不能做的事比如不能编造工具返回结果。我踩过的一个坑是系统提示词写得太温柔模型遇到不确定的情况就自己编。后来我在提示词里加了一句如果工具没有返回你需要的信息必须明确说明信息不足禁止推测编造情况明显减少。提示词里的禁止项比鼓励项更重要。4. 实操过程从零搭一个可用的 Harness Runtime4.1 最小可用版本50 行代码跑通循环先给一个能跑的最小版本用伪代码表示你可以直接映射到自己的技术栈def run_agent(user_input, max_turns15): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input} ] for turn in range(max_turns): response call_model(messages, toolsTOOL_SCHEMAS) if not response.tool_calls: return response.content messages.append(response.as_message()) for call in response.tool_calls: if is_duplicate(call, messages): messages.append(tool_error(call, 重复调用请换策略)) continue try: result execute_tool(call.name, call.args) messages.append(tool_result(call.id, result)) except Exception as e: messages.append(tool_error(call.id, str(e))) return 达到最大轮次任务未完成这 50 行里已经包含了循环、工具执行、错误处理、重复检测、轮次限制。先把这个跑通再往上加东西。我见过太多人一上来就设计复杂的多 Agent 架构结果连最基本的循环都没跑稳。4.2 加上状态压缩让长任务不爆 token在上面基础上加状态压缩核心逻辑是def maybe_compress(messages, threshold20000): if count_tokens(messages) threshold: return messages recent messages[-8:] older messages[:-8] summary call_model([ {role: system, content: 把以下对话压缩成200字以内的摘要保留关键事实和未完成的任务}, {role: user, content: serialize(older)} ]) return [{role: system, content: f历史摘要{summary}}] recent这里有个细节压缩时要把未完成的任务单独保留。我踩过的坑是压缩后模型忘了用户最初的要求开始答非所问。后来我在摘要 prompt 里明确要求保留用户原始诉求和当前进度问题就解决了。4.3 工具执行的安全边界工具执行是 Agent 最危险的部分因为它真的会动外部资源。我一般设三道边界白名单只有注册过的工具能执行模型传未知工具名直接拒绝。参数校验每个工具执行前校验参数类型和范围比如文件路径必须在允许目录内。超时控制每个工具执行设超时比如 30 秒超时直接返回错误不让整个 Agent 卡死。注意涉及写操作的工具写文件、发请求、改数据一定要加确认机制。我的做法是写操作先返回待确认让模型在下一轮明确请求执行或者直接交给人工确认。宁可多一轮不要写错数据。4.4 日志与可观测性出问题时你能查到什么Agent 出问题是常态关键是你能不能快速定位。我要求每个 Agent 必须记录这几类日志每轮输入输出模型看到了什么、返回了什么。工具调用记录调了什么工具、参数是什么、返回什么、耗时多少。状态变化消息历史长度、token 数、压缩触发点。异常堆栈任何报错的完整信息。这些日志我一般存成结构化 JSON方便检索。有一次线上 Agent 突然开始乱答我翻日志发现是某个工具返回了超长内容把上下文挤爆了。没有日志的话这种问题能查一整天。5. 常见问题与排查技巧实录5.1 模型不调工具直接编答案这是最高频的问题。模型明明有工具可用却直接凭记忆回答。原因通常有三个工具描述不够明确、系统提示词没强调必须用工具、或者模型本身能力不足。排查顺序先看工具描述是不是写清楚了什么时候用再看系统提示词有没有必须基于工具返回回答的硬性要求最后考虑换模型。我实测下来把工具描述从查询天气改成当用户询问任何地点的天气情况时必须调用此工具获取实时数据不调工具的情况能减少八成。5.2 工具调用参数总是错参数错误通常是因为参数名不直观、类型没约束、或者缺少示例。解决办法是在工具 schema 里加description和example把每个参数的含义和格式写清楚。比如日期参数明确写格式 YYYY-MM-DD例如 2024-01-15。另外参数校验失败时把具体的校验错误返回给模型它下一轮通常能自己修正。5.3 循环停不下来前面提过三道保险必须加最大轮次、重复检测、无进展检测。除此之外还有一个隐蔽原因模型在等一个永远不会来的工具结果。比如它调了一个异步工具但你的 Runtime 没等结果就进入下一轮。解决办法是确保工具执行是同步阻塞的或者在异步场景下明确等待。5.4 多轮之后质量下降这是上下文污染导致的。早期轮次的错误信息、无关内容会干扰后续推理。解决办法就是状态压缩把早期内容摘要化。另外我习惯在每轮开始时把当前任务目标重新强调一遍放在消息历史的最前面防止模型跑偏。5.5 常见问题速查表问题现象最可能原因快速排查不调工具直接答工具描述模糊/提示词没要求检查工具描述和系统提示词参数总出错参数定义不清/缺示例补 description 和 example循环停不下来缺终止条件/重复调用加轮次限制和重复检测多轮后质量降上下文污染启用状态压缩工具执行卡死缺超时控制每个工具加超时结果不稳定温度参数过高降到 0 到 0.35.6 几个我踩过的坑第一个坑是过早优化。我一开始就想着做多 Agent 协作结果两个 Agent 互相等对方输出死锁了。后来退回单 Agent 循环问题迎刃而解。架构不是越复杂越好是越匹配任务越好。第二个坑是忽视 token 成本。一个 ReAct 循环跑 15 轮每轮都带完整历史token 消耗是单次调用的几十倍。上线前一定要算清楚成本必要时用更小的模型做工具选择大模型只做最终生成。第三个坑是工具返回值太长。有个工具返回了整个表格几千行直接把上下文撑爆。后来我改成只返回前 20 行加一个共 N 行的提示模型需要更多再单独查。6. 架构演进路线什么时候该往上加一层6.1 从单轮到循环的触发信号什么时候该从单轮调用升级到 ReAct 循环我的判断标准是当任务需要根据中间结果决定下一步时。如果用户的问题需要先查 A 再根据 A 的结果决定查 B那单轮就不够了。反过来如果所有信息都能一次性拿到单轮更快更省。6.2 从循环到规划-执行的触发信号当任务步骤超过 5 步、且步骤之间有明确依赖时ReAct 循环会变得低效因为模型每步都要重新推理全局。这时候规划-执行分离更合适先让模型出一个完整计划再逐步执行每步只关注当前子任务。代价是计划可能僵化遇到意外需要重新规划。6.3 从单 Agent 到多 Agent 的触发信号多 Agent 只在两种情况下值得任务需要不同专业视角比如一个写代码一个审查或者需要并行处理独立子任务。如果只是任务复杂单 Agent 加更多工具通常就够了。多 Agent 的通信开销和调试难度是实打实的别为了架构好看而上。6.4 我的实际演进路径我自己的项目是这样走的第一版单轮调用第二版加 ReAct 循环和 3 个工具第三版加状态压缩和错误处理第四版才引入规划-执行分离。每一步都是被实际问题逼出来的不是提前设计的。架构应该跟着问题走而不是跟着潮流走。你现在如果还在第一版别急着跳到第四版先把循环跑稳把工具定义好把日志打全这些基础做扎实了往上加层是水到渠成的事。最后分享一个我一直在用的小技巧每次给 Agent 加新能力之前先手动模拟一遍完整流程把每一步的输入输出写下来。如果手动流程里出现了这里需要判断一下再决定那这个地方就是需要循环或分支的信号。手动跑不通的流程交给 Agent 也跑不通。这个习惯帮我省掉了大量无效开发。