从单次调用到Harness Runtime:生产级Agent架构演进指南 1. 从一次裸调用说起为什么单次 LLM 调用撑不起一个 Agent很多人第一次接触 Agent 这个概念都是从一行代码开始的把用户输入拼进 prompt调一次大模型接口拿到返回结束。这个模式在 demo 阶段完全够用甚至能骗过不少人——毕竟输出看起来挺像那么回事。但只要你想让它做点稍微复杂的事比如帮我查一下这个季度的销售数据生成图表然后写一份分析报告发到群里单次调用立刻崩盘。原因很直接大模型本身是无状态的它不记得上一轮说过什么也没有手去执行任何操作。你让它查数据它只能编一段看起来像数据的文字你让它发到群里它只能告诉你我已经发送了——实际上什么都没发生。这不是模型能力问题是架构问题。单次调用本质上是一个文本进、文本出的函数而 Agent 需要的是一个感知、决策、执行、反馈的闭环。我见过不少团队在这个阶段踩坑一开始用单次调用硬扛靠不断往 prompt 里塞上下文来模拟记忆靠正则匹配来解析模型意图靠 if-else 来路由不同任务。代码写到三千行的时候维护成本已经超过重写成本了。这时候才意识到需要一套真正的运行时架构来承载 Agent 的行为。所谓Harness Runtime你可以把它理解成 Agent 的操作系统。它负责管理对话历史、调度工具调用、处理异常重试、控制上下文窗口、协调多轮循环。模型只是这个系统里的一个组件而不是全部。这个认知转变很关键——从调模型变成跑系统思路完全不一样。这篇文章适合两类人看一类是已经写过单次调用、正准备往 Agent 方向走的开发者另一类是已经在做 Agent 但架构越写越乱、想重新梳理的工程师。我会从最基础的调用讲起一步步推到完整的 Harness Runtime 设计中间穿插我实际踩过的坑和选型逻辑。不堆概念只讲能落地的方案。2. 架构选型的核心决策点从简单到复杂的四条路线2.1 路线一单次调用加后处理适合什么场景最简单的架构就是一次 LLM 调用 规则后处理。用户输入进来拼一个固定模板的 prompt调模型拿到输出后用正则或 JSON 解析提取关键信息再交给下游代码执行。这条路线不是没有价值。如果你的任务满足以下条件它其实是最优解任务边界清晰、输出格式固定、不需要多轮交互、不需要外部工具。比如文本分类、情感分析、固定格式的信息抽取单次调用加后处理完全够用延迟低、成本低、调试简单。我做过一个工单自动分类的小工具就是这条路prompt 里写死分类体系和输出格式模型返回 JSON代码直接解析入库。整个系统不到两百行跑了半年没出过问题。关键判断标准是你的任务能不能在一次思考内完成。能就别上复杂架构不能才往下走。但要注意一个坑很多人为了省事把本该多步完成的任务硬塞进一次调用靠请一步一步思考来续命。这在任务简单时有效一旦步骤超过三步模型就开始丢步骤、编中间结果。我实测下来单次调用能稳定处理的逻辑步骤上限大概在 2 到 3 步超过这个数错误率明显上升。2.2 路线二固定流程编排用代码控制节奏当任务需要多步时第一个自然的升级是用代码编排固定流程。比如先抽取关键词再根据关键词检索再基于检索结果生成回答这三步用代码串起来每步调一次模型或工具。这条路线的好处是可控性极强。每一步的输入输出你都能检查出错能定位到具体环节重试也方便。缺点是灵活性差——流程写死了用户换个问法可能就走不通。比如你设计的是先检索再回答用户直接问你刚才说的那个数据来源是什么固定流程就懵了。我一般建议如果你的业务场景高度标准化比如客服 FAQ、表单填写、固定报表生成优先选这条路线。别被Agent这个词绑架觉得不用自主决策就不高级。工程上能确定性解决的问题永远不要交给概率。2.3 路线三ReAct 循环让模型自己决定下一步ReAct 是 Reasoning Acting 的缩写核心思路是让模型在每一轮输出思考和行动思考当前该做什么行动就是调用某个工具拿到结果后再进入下一轮思考。循环直到模型认为任务完成。这是目前最主流的 Agent 架构也是大多数框架的默认模式。它的优势是灵活——模型可以根据中间结果动态调整策略遇到意外情况能自己想办法。比如检索没找到相关内容它会换个关键词再试而不是直接失败。但 ReAct 的问题也很明显。第一循环次数不可控模型可能陷入死循环反复调同一个工具。第二每轮都要把完整历史塞进上下文token 消耗随轮次线性增长跑到十几轮的时候成本和延迟都很吓人。第三错误会累积前面一步理解错了后面全跟着错。我实际用下来ReAct 适合探索性任务比如帮我调研一下某个话题因为路径不固定。但如果是帮我订一张明天去某地的票这种目标明确的任务ReAct 反而容易绕远路不如固定流程直接。2.4 路线四完整 Harness Runtime把控制权收回来走到这一步你会发现纯 ReAct 的问题根源在于控制权完全交给了模型。模型说继续就继续说停就停说调什么工具就调什么工具。这在 demo 里很酷在生产环境里很危险。完整 Harness Runtime 的思路是把控制权收回到系统层模型只负责建议系统负责决策。具体来说Runtime 要管这些事循环控制最大轮次、超时、死循环检测系统说了算上下文管理历史压缩、关键信息提取、窗口裁剪不让 token 无限膨胀工具调度工具注册、参数校验、权限控制、并发限制错误处理重试策略、降级方案、人工介入触发条件状态持久化每一步的状态落盘支持中断恢复这套东西听起来复杂但它是从 demo 到生产必须跨过的坎。我见过太多 Agent 项目卡在演示很惊艳上线就翻车根因都是缺少这层 Runtime。模型再强也需要一个靠谱的司机来握方向盘。下面这张表可以帮你快速判断自己该选哪条路线路线适用场景核心优势主要风险单次调用后处理分类、抽取、固定格式生成简单、快、便宜无法处理多步任务固定流程编排标准化业务流程可控、可调试灵活性差ReAct 循环探索性、路径不固定任务灵活、自适应成本高、易失控完整 Runtime生产级复杂 Agent可控、可观测、可恢复实现复杂度高3. Harness Runtime 的核心模块拆解与实现要点3.1 循环控制器怎么防止 Agent 跑到天荒地老循环控制器是 Runtime 的心脏。它的职责很简单决定什么时候继续、什么时候停。但实现起来细节很多。最基本的控制是最大轮次限制。我一般设 10 到 15 轮超过就强制停止并返回当前最佳结果。这个数字不是拍脑袋定的——我统计过几十个真实任务90% 的合理任务在 8 轮内完成超过 15 轮的要么是任务本身有问题要么是模型陷入了循环。光有轮次限制不够还要有重复检测。具体做法是记录每一轮的行动签名工具名 参数哈希如果连续两轮签名相同说明模型在原地打转直接中断。这个机制救过我很多次——有一次模型反复调用同一个搜索工具参数一模一样就是因为第一次返回结果它没理解以为没搜到。没有重复检测的话它能调到 token 耗尽。还有一个容易被忽略的是超时控制。整个 Agent 执行要有总超时单个工具调用也要有超时。我遇到过某个外部接口偶发卡死没有超时的话整个 Agent 就挂在那里用户等半天没反应。总超时建议设在 60 到 120 秒单工具超时 10 到 30 秒具体看业务容忍度。class LoopController: def __init__(self, max_turns12, max_repeat2, total_timeout90): self.max_turns max_turns self.max_repeat max_repeat self.total_timeout total_timeout self.action_history [] self.start_time None def should_continue(self, turn, action_signature): if turn self.max_turns: return False, 达到最大轮次 if time.time() - self.start_time self.total_timeout: return False, 总超时 recent self.action_history[-self.max_repeat:] if len(recent) self.max_repeat and all(s action_signature for s in recent): return False, 检测到重复行动 self.action_history.append(action_signature) return True, 这段代码不复杂但它是生产环境的保命符。我建议把每次中断的原因都记录下来方便后续分析——如果某个原因频繁出现说明 prompt 或工具设计有问题。3.2 上下文管理器token 预算怎么花在刀刃上上下文管理是 Runtime 里最考验工程能力的地方。模型的上下文窗口是有限的而 Agent 跑起来历史会越来越长。不做管理的话要么超限报错要么成本爆炸。我的做法是分层管理第一层是系统提示词固定不变包含角色定义、工具说明、输出格式要求。这部分永远保留不参与裁剪。第二层是任务目标也就是用户最初的请求。这部分也永远保留因为它是 Agent 的北极星丢了就迷失方向了。第三层是近期历史保留最近 N 轮的完整对话。N 一般取 3 到 5保证模型能看清最近的上下文。第四层是远期摘要把更早的历史压缩成一段摘要。摘要不是简单截断而是让模型自己总结到目前为止发生了什么、得到了哪些关键信息、还差什么。这里有个实操技巧摘要要在轮次切换时异步生成不要阻塞主流程。我一开始是同步做的结果每几轮就卡一下用户体验很差。改成异步后主流程用上一轮的摘要新摘要后台生成下一轮再用流畅多了。还有一个细节是工具返回结果的裁剪。有些工具返回一大坨 JSON 或 HTML直接塞进上下文能占几千 token。我的做法是在工具层就做预处理只返回模型真正需要的字段。比如搜索工具只返回标题、摘要、URL不返回完整网页内容。这一层过滤能省下大量 token而且不影响模型决策。提示上下文管理没有银弹核心原则是模型当前决策需要什么就给它什么。不要为了省 token 把关键信息裁掉也不要为了保险把所有历史都塞进去。这个平衡点需要根据你的业务反复调。3.3 工具调度层注册、校验、执行、回传的完整链路工具是 Agent 的手脚。工具调度层要管四件事注册、校验、执行、回传。注册就是维护一个工具清单每个工具包含名称、描述、参数 schema。描述很重要模型靠它决定什么时候用哪个工具。我见过很多工具描述写得含糊导致模型该用的时候不用、不该用的时候乱用。好的工具描述应该包含这个工具做什么、什么时候用、参数怎么填、返回什么。校验是在模型给出工具调用请求后检查参数是否合法。模型经常犯的错包括参数类型不对该传数字传了字符串、必填参数缺失、参数值超出范围。这些在调工具之前就要拦住返回明确的错误信息让模型重试而不是让工具自己报错。执行环节要注意隔离和限流。工具执行可能失败、可能慢、可能消耗资源不能让它拖垮整个 Runtime。我的做法是每个工具调用都包一层带超时、带重试、带熔断。某个工具连续失败几次就暂时禁用避免反复撞墙。回传环节是把工具结果格式化后塞回上下文。这里的关键是结果要简洁且结构化。我一般要求工具返回 JSON然后 Runtime 统一格式化成模型容易理解的文本。不要直接把原始返回丢进去模型解析起来费劲还容易误解。class ToolRegistry: def __init__(self): self.tools {} self.failure_count {} def register(self, name, description, schema, handler): self.tools[name] { description: description, schema: schema, handler: handler } def execute(self, name, params, timeout15): if name not in self.tools: return {error: f工具 {name} 不存在} if self.failure_count.get(name, 0) 3: return {error: f工具 {name} 暂时不可用} try: validated self._validate(params, self.tools[name][schema]) result self._run_with_timeout( self.tools[name][handler], validated, timeout ) self.failure_count[name] 0 return {result: result} except Exception as e: self.failure_count[name] self.failure_count.get(name, 0) 1 return {error: str(e)}这套机制看起来繁琐但它把工具的不确定性关进了笼子。模型可以随便想但真正执行的时候系统说了算。3.4 状态持久化让 Agent 能中断、能恢复、能审计状态持久化是很多人一开始不做、后来追悔莫及的部分。Agent 跑一轮可能几十秒中间涉及多次模型调用和工具执行。如果进程崩了、服务重启了没有持久化的话整个任务就丢了用户得重来。我的做法是每一步都落盘。具体来说每轮结束后把当前状态写进数据库或缓存状态包含任务 ID、当前轮次、完整历史、已收集的信息、下一步计划。这样即使中断也能从最近的状态恢复。持久化还有个附带好处是可审计。线上出问题时你能回放整个执行过程看到模型每一步想了什么、调了什么工具、拿到什么结果。这对排查问题太重要了。我遇到过用户投诉Agent 给的结果不对没有执行记录的话根本没法定位有了记录一看就知道是某一步工具返回了脏数据模型基于脏数据做了错误推理。存储选型上Redis 适合做运行时状态缓存关系型数据库适合做长期审计日志。两者配合使用前者保证恢复速度后者保证可追溯性。注意状态里可能包含用户敏感信息落盘前要做脱敏或加密。这个不是可选项是合规底线。4. 完整实操从零搭一个可运行的 Agent Runtime4.1 环境准备与依赖选型先说环境。我用的是 Python 3.10主要依赖三个库模型 SDK、HTTP 客户端、序列化工具。模型 SDK 看你用哪家接口大同小异HTTP 客户端我推荐 httpx支持异步和超时控制序列化用 pydantic做参数校验和状态序列化都很方便。目录结构我一般这样组织agent_runtime/ ├── core/ │ ├── loop_controller.py │ ├── context_manager.py │ └── state_store.py ├── tools/ │ ├── registry.py │ └── builtin.py ├── llm/ │ └── client.py ├── config.py └── main.py这个结构的好处是职责清晰。core 放 Runtime 核心逻辑tools 放工具相关llm 放模型调用封装。改哪块找哪块不会一团乱麻。配置方面我建议把模型参数、超时阈值、轮次上限、工具开关都放到配置文件里不要硬编码。线上调参的时候你会感谢自己做了这件事。4.2 核心循环的代码实现核心循环是整个 Runtime 的主干。它的逻辑是组装上下文 → 调模型 → 解析输出 → 如果是工具调用就执行 → 把结果塞回上下文 → 判断是否继续。class AgentRuntime: def __init__(self, llm_client, tool_registry, config): self.llm llm_client self.tools tool_registry self.config config self.loop LoopController( max_turnsconfig.max_turns, total_timeoutconfig.total_timeout ) self.context ContextManager(config.context_window) self.state StateStore(config.state_backend) def run(self, task_id, user_input): self.context.init_task(user_input) self.state.save(task_id, self.context.snapshot()) for turn in range(self.config.max_turns): messages self.context.build_messages() response self.llm.chat(messages, toolsself.tools.schemas()) if response.is_tool_call: signature self._action_signature(response) should_go, reason self.loop.should_continue(turn, signature) if not should_go: return self._finalize(reason) result self.tools.execute( response.tool_name, response.tool_params ) self.context.add_tool_result(response, result) else: self.context.add_assistant_message(response.content) if self._is_task_complete(response): return self._finalize(完成) self.state.save(task_id, self.context.snapshot()) return self._finalize(达到轮次上限)这段代码是骨架实际用的时候每个方法都要填充细节。但结构就是这样清晰、可读、每步都有记录。4.3 工具注册与调用的落地示例光有框架不够得有具体工具才能跑起来。我拿两个最常用的工具举例搜索工具和计算工具。搜索工具负责从外部获取信息。实现上就是调一个搜索接口把结果整理成结构化数据返回。关键是结果要精简只返回标题、摘要、链接不要返回完整网页。def search_handler(query, top_k5): raw search_api.query(query, limittop_k) return [ {title: r[title], snippet: r[snippet], url: r[url]} for r in raw ] registry.register( namesearch, description根据关键词搜索信息返回相关结果的标题和摘要。当需要获取外部知识时使用。, schema{query: string, top_k: int, optional, default 5}, handlersearch_handler )计算工具负责做数学运算。别小看这个模型做算术经常出错交给代码算才靠谱。def calc_handler(expression): allowed set(0123456789-*/(). ) if not set(expression) allowed: raise ValueError(表达式包含非法字符) return eval(expression, {__builtins__: {}}, {}) registry.register( namecalculate, description计算数学表达式支持加减乘除和括号。当需要精确计算时使用。, schema{expression: string, 如 (35)*2}, handlercalc_handler )工具描述我特意写得比较具体因为模型选工具全靠这段描述。描述里说清楚什么时候用比说清楚这个工具是什么更重要。4.4 一次完整任务的执行过程记录假设用户输入是帮我算一下如果一件商品原价 299打八折后再减 50最终多少钱第一轮模型看到任务判断需要计算输出工具调用calculate(299 * 0.8 - 50)。Runtime 校验参数合法执行计算得到 189.2把结果塞回上下文。第二轮模型看到计算结果判断任务完成输出最终回答最终价格是 189.2 元。整个过程两轮耗时不到三秒。这个例子简单但它展示了完整链路理解 → 决策 → 执行 → 反馈 → 输出。如果任务复杂一点比如帮我查一下某地明天的天气如果下雨就提醒我带伞就会涉及搜索工具调用、结果解析、条件判断可能跑三到四轮。Runtime 的每个模块都会在这几轮里发挥作用循环控制器管轮次上下文管理器管历史工具调度层管执行状态存储管恢复。我实测下来大部分日常任务在 3 到 6 轮内完成token 消耗在几千到一万多之间。超过这个范围的要么任务本身太复杂需要拆解要么 prompt 需要优化。5. 常见问题与排查技巧实录5.1 模型不调工具只输出文字怎么办这是最常见的问题。模型明明该调工具却直接编了一段回答。原因通常有三个工具描述不清楚、系统提示词没强调、模型本身能力不足。排查顺序先看工具描述是不是写得太抽象模型不知道什么时候用再看系统提示词有没有明确要求需要外部信息时必须调工具不要自己编最后看模型换个更强的模型试试如果换了就好说明是能力问题。我的经验是在系统提示词里加一句如果你不确定优先调工具而不是猜测能解决大部分问题。模型有讨好用户的倾向喜欢直接给答案你得明确告诉它调工具才是对的。5.2 工具调用参数总是出错怎么治参数错误的表现是类型不对、必填缺失、值超范围。根因通常是schema 定义不清晰或模型对参数理解有偏差。治法分两步。第一步把 schema 写详细每个参数都加描述和示例。比如不要只写date: string要写date: string, 格式 YYYY-MM-DD, 如 2024-01-15。第二步在 Runtime 里做严格校验参数不对就返回明确错误让模型重试。错误信息要具体不要只说参数错误要说date 参数格式不对应该是 YYYY-MM-DD。我还会在 prompt 里放一两个正确调用的示例模型照着抄出错率明显下降。5.3 Agent 陷入死循环的三种典型场景死循环是 Agent 的经典病。我总结了三类第一类工具返回空结果模型反复重试。比如搜索没搜到模型换个词再搜还是没搜到再换……治法是设重试上限同一个工具连续失败两次就强制换策略或终止。第二类模型在两个工具之间反复横跳。比如先搜索再计算再搜索再计算……治法是检测行动序列的周期性发现循环就中断。第三类模型认为任务没完成但实际已经完成。比如它一直在确认某个信息反复调同一个查询工具。治法是加一个完成度判断环节让模型明确输出任务是否完成而不是靠它自己停。提示死循环的根因往往是任务定义模糊。用户说帮我处理一下这个模型不知道处理到什么程度算完。把任务目标写具体能预防大部分死循环。5.4 上下文超限的应急处理方案上下文超限通常发生在长任务里。应急处理有三招第一招立即裁剪最老的历史保留系统提示、任务目标、最近几轮。这是最快的止血方法但可能丢信息。第二招触发摘要压缩把老历史压成一段话。这个稍微慢一点但保留的信息更多。第三招把中间结果外置比如把已收集的数据存到外部存储上下文里只留一个引用 ID。这个最彻底但实现复杂。我一般按这个顺序降级先摘要摘要还不够就裁剪裁剪还不够就外置。平时就要监控 token 使用量别等到超限了才处理。下面这张表汇总了常见问题和对应解法问题现象可能原因排查方向解决方案不调工具只输出文字描述不清/提示词弱检查工具描述和系统提示强化描述加优先调工具指令参数反复出错schema 模糊检查参数定义补详细描述和示例严格校验死循环任务模糊/无重试上限看行动历史设重试上限加周期检测上下文超限历史无管理看 token 增长曲线摘要压缩结果外置执行中断丢失无持久化检查状态存储每步落盘支持恢复5.5 性能与成本的平衡技巧Agent 跑起来成本和延迟是两个绕不开的指标。我的优化经验有这么几条能用小模型的地方别用大模型。比如意图识别、参数抽取这种简单任务小模型完全够用成本能降一个数量级。只在需要复杂推理的环节用大模型。工具结果能缓存就缓存。同一个查询短时间内重复调用直接返回缓存结果省一次调用。并行化能并行的工具调用。如果模型一轮里请求了多个互不依赖的工具并发执行别串行等。prompt 能精简就精简。系统提示词不是越长越好冗余的描述既费 token 又干扰模型。我定期会 review 提示词删掉没用的部分。我实测过一个优化案例同样的任务优化前平均 8 轮、消耗 15000 token优化后平均 4 轮、消耗 6000 token延迟从 25 秒降到 10 秒。优化空间往往比想象的大。6. 架构演进的一些个人体会做 Agent 这几年我最大的体会是架构复杂度要跟着业务复杂度走不要提前过度设计。我见过太多团队一上来就搭全套 Runtime结果业务根本用不上那些功能白白增加维护成本。我的建议是渐进式演进先用单次调用验证需求需求撑不住了升级到固定流程流程不够灵活了引入 ReActReAct 失控了再上完整 Runtime。每一步都是被真实问题推着走而不是被概念推着走。另一个体会是可观测性比功能更重要。Agent 的行为是概率性的出问题是常态。没有完善的日志、追踪、回放能力排查问题就是盲人摸象。我现在的习惯是任何新功能上线前先确保它的每一步都能被记录和回放。最后说个具体的别迷信框架。市面上 Agent 框架很多但每个业务的需求都不一样框架能覆盖 70%剩下 30% 的定制往往比从零写还麻烦。我的做法是核心 Runtime 自己写外围工具用现成的。核心逻辑掌握在自己手里才不会被框架的更新节奏绑架。这套 Runtime 我迭代了大概半年从最初的两百行到现在两千多行每一行都是被真实问题逼出来的。它不完美但足够稳。如果你也在做类似的事希望这些经验能帮你少走点弯路。