资讯详情

从裸调用到Harness Runtime:Agent架构四层演进与生产级实践

📅 2026/10/20 7:58:06 | 华诺云谱 👁 阅读
从裸调用到Harness Runtime:Agent架构四层演进与生产级实践
1. 从一次裸调用说起为什么单次 LLM 调用撑不起一个 Agent很多人第一次接触 Agent脑子里想的其实很简单给大模型一个系统提示词把工具描述塞进去模型返回一个函数名和参数我执行完再把结果喂回去循环几轮这不就是个 Agent 了吗我一开始也是这么想的甚至用不到一百行代码就搭出了一个能查天气、能算数的玩具 Agent。但真正把它放到稍微复杂一点的场景里跑问题就全冒出来了。最直观的崩溃点在于状态管理。单次 LLM 调用是无状态的你每次都得把完整的历史消息重新塞进上下文。当对话轮次一多工具返回的结果一长上下文窗口很快就被撑爆。这时候你要么截断历史丢失关键信息要么做摘要引入信息损耗要么上向量检索增加延迟和不确定性。这三条路没有一条是免费的而裸调用本身不提供任何解决方案。第二个崩溃点是错误恢复。模型偶尔会返回格式错误的 JSON或者调用一个根本不存在的工具或者参数类型对不上。裸调用模式下这些错误要么直接抛异常中断整个流程要么你得在业务代码里写一堆 try-catch 和重试逻辑。我见过最离谱的一个实现重试逻辑写了三百多行比 Agent 主体还长而且每次重试都是从头再来前面已经成功的步骤全部白费。第三个崩溃点是可观测性。当 Agent 跑出问题时你根本不知道是哪一步出的错。是提示词没写好是工具描述有歧义是模型选错了还是上下文被污染了裸调用不给你任何中间状态的记录你只能靠打印日志而日志在并发场景下又是一团乱麻。所以从一次 LLM 调用到完整的 Harness Runtime中间隔的不是加几行代码而是一整套运行时基础设施。Harness 这个词在软件工程里本来指测试框架但在 Agent 语境下它指的是包裹在模型外面的那层马具——负责调度、状态、工具、错误、观测、安全的全套机制。这篇文章我就把这层马具拆开从最简形态一路讲到生产级实现把每个环节的技术选型和踩坑经验都摊开说。2. Agent 架构的四层演进从裸调用到 Harness Runtime2.1 第一层裸调用加手工循环最原始的形态长这样一个 while 循环每次把消息历史发给模型解析返回如果是工具调用就执行把结果追加到历史继续循环如果是普通文本就退出。这个形态的代码量大概在五十到一百行适合做 Demo 和验证想法。它的核心问题在于所有职责耦合在一起。循环控制、消息管理、工具执行、错误处理全在一个函数里。你想换个模型得改代码。你想加个新工具得改代码。你想记录一下每步耗时还是得改代码。这种耦合在原型阶段无所谓但一旦要维护就是灾难。我个人的经验是这个形态最多撑到三个工具、五轮对话。超过这个规模代码就开始失控。所以它只适合做概念验证不要指望它能上生产。2.2 第二层引入工具注册与消息抽象第二层的核心改进是把工具和消息抽象出来。工具不再硬编码在 if-else 里而是注册到一个工具表里每个工具包含名称、描述、参数 schema、执行函数。消息也不再是裸的字典列表而是封装成对象支持角色、内容、工具调用、工具结果等结构化字段。这一层带来的最大好处是可扩展性。加一个新工具只需要注册不需要改主循环。换模型也只需要换一个适配器因为消息格式已经统一了。我实测下来这一层能撑到十几个工具、几十轮对话基本覆盖大部分中小型应用场景。但这一层仍然缺少状态持久化和错误恢复。如果进程崩了整个对话历史就没了。如果某一步工具执行失败你只能选择整体重试或者整体放弃没法做细粒度的恢复。2.3 第三层加入状态机与检查点第三层的关键词是状态机。把 Agent 的执行过程建模成状态转移初始化、思考、工具调用、工具执行、结果注入、完成、失败。每个状态转移都可以被打断、被记录、被恢复。检查点机制让你可以在任意一步保存快照崩溃后从最近的检查点恢复而不是从头再来。这一层还引入了重试策略和降级策略。比如工具调用失败时可以先重试两次再降级到让模型换一个工具最后才标记为失败。这些策略如果写在业务代码里会非常乱但放在状态机里就很自然因为每个状态转移本来就是一个决策点。我踩过的一个坑是状态机设计得太细导致状态数量爆炸维护成本反而更高。后来我学乖了状态粒度控制在一个状态对应一个可观测的执行单元这个级别既不会太粗导致无法恢复也不会太细导致状态爆炸。2.4 第四层完整 Harness Runtime第四层就是完整的 Harness Runtime它在第三层的基础上补齐了可观测性、安全护栏、多 Agent 协作、资源配额这几块。可观测性包括结构化日志、链路追踪、指标采集安全护栏包括输入输出过滤、工具权限控制、敏感操作审批多 Agent 协作包括消息路由、任务分发、结果聚合资源配额包括 token 预算、时间预算、调用次数预算。这一层的复杂度已经接近一个小型分布式系统所以它通常不是从零手写的而是基于某个框架扩展。但理解每一层的原理仍然很重要因为框架只是把通用逻辑封装好了你的业务特殊性还是得自己填。下面这张表把四层的核心差异列出来方便你对照自己的项目判断该停在哪一层。层级核心能力适用规模典型代码量主要痛点第一层手工循环Demo 验证50-100 行职责耦合无法维护第二层工具注册与消息抽象中小应用300-800 行无持久化无细粒度恢复第三层状态机与检查点中大型应用1500-3000 行状态设计容易过度第四层完整 Harness Runtime生产级系统框架加扩展复杂度高调试困难3. 核心组件拆解Harness Runtime 里到底该放什么3.1 调度器Agent 的大脑回路调度器负责决定下一步做什么。在最简单的实现里它就是调用模型看返回执行工具再调用模型这个循环。但在生产级实现里调度器要处理的事情多得多。首先是终止条件。模型什么时候算完成任务是返回了特定标记是达到了最大轮次还是某个外部条件满足我见过太多 Agent 因为没有明确的终止条件而陷入死循环烧掉大量 token 才被发现。我的做法是设置三重保险最大轮次限制、最大 token 预算、最大时间预算任意一个触发就强制终止并返回当前最佳结果。其次是并发控制。有些工具调用是可以并行的比如同时查三个城市的天气。但并行会带来结果顺序问题、错误传播问题、资源竞争问题。我的经验是只对无副作用且相互独立的工具调用做并行而且并行度要限制通常不超过四到八个否则模型处理并行结果时容易混乱。第三是中断与恢复。用户可能中途取消系统可能重启这些都需要调度器支持从检查点恢复。实现上就是把调度器的状态序列化恢复时反序列化再继续。这里有个细节工具执行到一半被中断怎么办如果工具有副作用就得考虑幂等性或者补偿机制。3.2 上下文管理器比你想的更重要上下文管理是 Harness Runtime 里最容易被低估的组件。很多人以为它就是把消息拼起来发给模型但实际上它要解决的是在有限窗口里塞进最有用的信息这个问题。我常用的策略是分层管理。第一层是系统提示词永远保留包含角色定义、行为准则、工具使用规范。第二层是近期对话保留最近 N 轮N 根据任务复杂度动态调整。第三层是关键记忆从历史中提取的事实、决策、中间结果用结构化格式存储。第四层是检索结果根据当前问题从外部知识库拉取的相关内容。这四层的优先级从高到低当窗口不够时从最低优先级开始裁剪。裁剪不是简单删除而是先做摘要压缩压缩后还是不够才删除。我实测下来这套策略能把有效上下文利用率提升百分之四十以上。还有一个容易忽略的点是工具结果的格式化。工具返回的原始数据往往很冗长比如一个 API 返回的 JSON 可能有几十个字段但模型只需要其中三五个。在注入上下文之前做一次字段筛选和格式简化能省下大量 token。这个工作最好在工具适配器里做而不是在上下文管理器里做因为不同工具的筛选逻辑不一样。3.3 工具层从函数注册到权限沙箱工具层的演进路径是硬编码函数 → 注册表 → 带 schema 的注册表 → 带权限和沙箱的注册表。带 schema 的注册表是基础每个工具要有名称、描述、参数 schema、返回值 schema。描述要写得让模型能准确判断什么时候用这个工具参数 schema 要严格否则模型会传错类型。权限和沙箱是生产级才需要的。权限控制指的是哪些工具可以被哪些 Agent 调用哪些工具需要用户确认哪些工具有调用频率限制。沙箱指的是工具执行要在隔离环境里不能直接访问主进程的资源。我见过一个案例某个工具能执行任意 shell 命令结果模型被诱导执行了危险操作把整个服务搞挂了。所以工具层一定要有白名单机制只允许注册过的工具被执行。工具描述怎么写也是个学问。太短了模型不知道什么时候用太长了占上下文。我的经验是控制在五十到一百五十字包含三要素这个工具做什么、什么时候用、参数怎么填。举个例子一个查天气的工具描述可以写成查询指定城市的当前天气。当用户询问天气、温度、是否下雨时使用。参数 city 为城市名称格式为中文或英文城市名。3.4 记忆系统短期、长期与工作记忆记忆系统分三种。短期记忆就是当前对话的消息历史生命周期是单次会话。长期记忆是跨会话持久化的知识通常存在向量数据库里通过语义检索召回。工作记忆是当前任务相关的临时信息比如中间计算结果、待办事项列表生命周期是单个任务。短期记忆的管理前面讲过了重点是裁剪和压缩。长期记忆的关键是写入策略和召回策略。写入不能什么都写否则噪音太大召回不能只靠语义相似度还要考虑时间衰减和重要性权重。我的做法是给每条记忆打三个分相关性、时效性、重要性召回时加权排序。工作记忆最容易被忽略但它对复杂任务特别重要。比如一个多步推理任务中间结果如果只放在上下文里很容易被后续对话冲掉。显式地维护一个工作记忆区把关键中间结果结构化存储能显著提升任务完成率。我通常用 JSON 格式存工作记忆字段包括任务目标、已完成步骤、待完成步骤、关键数据。3.5 可观测性没有它就是在盲飞可观测性包括三块日志、追踪、指标。日志要结构化每条日志包含时间戳、会话 ID、步骤 ID、事件类型、事件内容。这样出问题时可以按会话或步骤过滤快速定位。追踪要覆盖完整链路从用户输入到最终输出中间每一次模型调用、每一次工具执行都要有 span。span 里记录输入、输出、耗时、token 消耗、错误信息。我用过的方案里OpenTelemetry 的语义约定最适合这种场景因为 Agent 的执行天然就是一棵调用树。指标要采集关键数值每轮 token 消耗、工具调用成功率、平均响应延迟、任务完成率、错误率。这些指标要能按时间、按模型、按工具维度聚合方便发现趋势和异常。我踩过的一个坑是日志打得太细导致存储成本爆炸打得太粗出问题又查不到。后来我定了个原则每个状态转移打一条 INFO 日志每个错误打一条 ERROR 日志模型输入输出打 DEBUG 日志且默认关闭。这样正常运行时日志量可控出问题时可以临时开 DEBUG。4. 实操落地从零搭一个最小可用 Harness4.1 环境准备与依赖选型先说语言和框架。Python 是首选因为生态最全大部分模型 SDK 和工具库都是 Python 优先。如果你对性能有极致要求可以考虑 Go 或 Rust但开发效率会下降不少。我的建议是除非有明确的性能瓶颈否则用 Python。核心依赖我列一下模型 SDK官方提供的就行、HTTP 客户端httpx 或 requests、数据校验pydantic、异步框架asyncio 或 anyio、日志structlog 或标准 logging、追踪opentelemetry-sdk。向量检索如果需要加一个向量库客户端。这些依赖加起来不超过十个保持精简很重要因为 Harness 本身应该是薄薄一层不应该引入太多外部依赖。版本管理用虚拟环境依赖锁定用 lock 文件。我见过因为依赖版本漂移导致 Agent 行为变化的案例所以锁定版本是必须的。4.2 消息与工具的数据结构设计消息结构我用 pydantic 定义核心字段包括rolesystem/user/assistant/tool、content文本内容、tool_calls工具调用列表、tool_call_id工具结果对应的调用 ID、metadata额外信息如时间戳、token 数。工具结构包括name、description、parametersJSON Schema、handler执行函数、timeout、retries、requires_confirmation。这里有个细节tool_calls 和 tool_call_id 的对应关系一定要维护好。模型可能一次返回多个工具调用每个调用有唯一 ID工具结果必须带上对应的 ID否则模型无法把结果和调用对应起来。我见过因为 ID 对不上导致模型反复调用同一个工具的 bug排查了很久。4.3 调度循环的实现与终止条件调度循环的伪代码大概是这样async def run_agent(session, max_turns20, max_tokens100000): turns 0 tokens_used 0 while turns max_turns and tokens_used max_tokens: context context_manager.build(session) response await model_client.call(context) tokens_used response.usage.total_tokens session.append(response.message) if response.has_tool_calls: results await execute_tools(response.tool_calls) session.append_tool_results(results) turns 1 continue else: return response.content return session.last_best_answer()关键点在于终止条件的判断。除了轮次和 token还要考虑重复检测如果模型连续三次调用同一个工具且参数相同大概率是卡住了应该强制终止。还有进展检测如果连续几轮没有产生新的有效信息也应该终止。4.4 工具执行的错误处理与重试工具执行要包一层错误处理。错误分三类可重试错误网络超时、临时限流、不可重试错误参数错误、权限不足、未知错误。可重试错误用指数退避重试重试次数控制在两到三次。不可重试错误直接把错误信息返回给模型让模型决定下一步。未知错误记录详细日志返回通用错误信息给模型。这里有个技巧错误信息要写得对模型友好。不要返回堆栈跟踪而是返回工具 X 执行失败原因是 Y建议尝试 Z。这样模型能根据错误信息调整策略而不是一脸懵。4.5 上下文裁剪的触发时机与策略上下文裁剪的触发时机有两个调用模型前和工具结果注入后。调用模型前裁剪是为了保证不超窗口工具结果注入后裁剪是为了及时清理冗余信息。裁剪策略我按优先级排序先删最旧的工具结果保留最近三轮的再压缩中间对话用模型做摘要最后才动系统提示词和近期对话。系统提示词永远不删近期对话至少保留三轮。摘要压缩有个坑摘要本身也要消耗 token而且摘要可能丢失关键细节。所以我的做法是只有当裁剪后仍然超窗口时才做摘要而且摘要只针对中间对话不针对工具结果。工具结果要么保留要么删除不做摘要因为工具结果的结构化信息摘要后往往就失真了。5. 常见问题与排查技巧实录5.1 模型不调用工具或调用错误工具这是最常见的问题原因通常有三个工具描述不清楚、系统提示词没强调工具使用、模型能力不足。排查顺序先看工具描述是不是太模糊或者太宽泛再看系统提示词有没有明确说需要外部信息时必须调用工具最后换一个能力更强的模型试试。如果换了模型就好了那就是模型能力问题要么换模型要么在提示词里加 few-shot 示例。我踩过的一个坑是工具描述里用了太多专业术语模型理解不了。后来改成大白话调用准确率立刻上去了。所以工具描述要写给模型看不是写给人看。5.2 工具调用参数格式错误模型返回的参数类型和 schema 对不上比如该传整数传了字符串该传数组传了单个值。这个问题在弱模型上特别常见。解决办法有两个一是在 schema 里把类型写清楚用 enum 限制取值范围二是在工具执行前做一次参数校验和类型转换能转就转不能转就返回错误让模型重试。我通常两个都做双保险。5.3 上下文超限与信息丢失上下文超限的根因是信息增长快于窗口容量。除了前面讲的裁剪策略还有一个办法是主动压缩在工具结果注入前就做一次精简只保留模型需要的字段。这个工作放在工具适配器里做效果最好。信息丢失的排查比较难因为丢失往往是渐进的。我的做法是在关键步骤打标记比如这里应该记得用户说过 X如果后续模型忘了 X就能定位到是哪次裁剪导致的。5.4 死循环与重复调用死循环的典型表现是模型反复调用同一个工具或者反复输出同样的内容。根因通常是任务无法完成但模型不知道放弃或者工具返回的结果让模型误以为需要重试。解决办法加重复检测连续相同调用超过阈值就强制终止在系统提示词里明确说如果多次尝试仍无法完成请返回当前最佳结果并说明原因工具返回错误时明确告诉模型这个错误不可重试请换一种方式。5.5 排查速查表现象可能原因排查方法解决方向不调用工具描述不清/提示词弱/模型弱检查描述和提示词换模型测试改描述加示例换模型参数格式错schema 不严/模型弱看返回参数和 schema 差异加校验加类型转换上下文超限信息增长快/裁剪不及时看 token 消耗曲线主动压缩优化裁剪死循环无终止条件/错误不可重试看调用序列是否重复加重复检测改错误提示结果不准确上下文污染/工具结果差看注入的上下文内容清理上下文优化工具6. 架构选型的决策框架什么时候该上哪一层6.1 按任务复杂度选层任务复杂度是最核心的决策依据。如果任务只是单轮问答加一两个工具调用第二层就够了。如果任务需要多轮推理、多个工具协作、中途可能失败需要恢复那就得上第三层。如果任务要跑在生产环境、有并发、有 SLA 要求那必须上第四层。我通常用一个简单的判断标准如果任务失败一次的代价你能接受就用低一层如果不能接受就用高一层。比如内部工具失败了大不了重跑第二层足够但面向用户的产品失败一次用户就流失了那就得上第四层。6.2 按团队规模选层团队规模也很关键。一两个人的小团队维护第四层的成本太高容易陷入基础设施还没搭好业务已经黄了的困境。我的建议是小团队从第二层起步遇到具体问题再往上加不要一开始就追求完整架构。中大团队可以考虑直接上第三层或第四层因为有人力维护而且架构统一后协作效率更高。但要注意架构统一不等于过度设计还是要按实际需求来。6.3 按迭代速度选层迭代速度快的项目架构要轻因为重架构会拖慢迭代。迭代速度慢但要求稳定的项目架构可以重一些因为稳定性优先。我见过一个反例一个快速迭代的创业项目一开始就搭了完整的 Harness Runtime结果每次改需求都要动好几层代码迭代速度被拖垮了。后来砍到第二层速度立刻上来了。所以架构要匹配迭代节奏不能盲目追求先进。6.4 选型决策表维度第二层第三层第四层任务复杂度低中高失败代价低中高团队规模1-3 人3-10 人10 人以上迭代速度快中慢并发要求低中高可观测要求低中高这张表不是绝对的只是给个参考。实际选型还要结合具体场景比如有些任务复杂度不高但失败代价极高比如涉及资金操作那也得往高层走。7. 我踩过的坑与实战心得7.1 不要过早抽象我一开始搭 Harness 的时候想着要设计得优雅一点于是把调度器、上下文、工具、记忆全抽象成接口每个接口好几个实现类。结果代码量爆炸调试困难改一个功能要动五六个文件。后来痛定思痛砍掉所有不必要的抽象只保留最核心的接口代码量少了三分之二可维护性反而更好。教训是抽象是为了解决重复问题不是为了看起来专业。没有重复的地方不要抽象等重复出现了再抽象也不迟。7.2 日志要打但要有策略前面提过日志的坑这里再强调一下。日志的核心是在正确的地方打正确的信息。我的策略是状态转移打 INFO错误打 ERROR模型输入输出打 DEBUG。DEBUG 默认关闭需要时通过环境变量开启。还有一个技巧是给日志加会话 ID 和步骤 ID这样出问题时可以按会话或步骤过滤快速定位。没有这两个 ID日志就是一盘散沙。7.3 工具描述要反复打磨工具描述的质量直接决定调用准确率。我通常会把工具描述写好后拿十几个典型问题测试看模型能不能正确选择工具。如果准确率低于百分之九十就回去改描述加示例改措辞直到准确率达标。这个过程很枯燥但回报很高。一个打磨好的工具描述能让整个 Agent 的成功率提升十几个百分点。7.4 终止条件要冗余终止条件不能只有一个要有多个冗余。我通常设置最大轮次、最大 token、最大时间、重复检测、无进展检测五个条件任意一个触发就终止。这样即使某个条件失效其他条件也能兜底。我见过因为只设了最大轮次结果模型每轮消耗大量 token轮次没到就把预算烧光的案例。所以多条件冗余是必须的。7.5 测试要覆盖异常路径正常路径的测试好写异常路径的测试容易被忽略。但 Agent 的异常路径特别多模型返回格式错误、工具超时、工具返回错误、上下文超限、网络中断。这些都要有测试用例。我的做法是写一个故障注入模块可以模拟各种异常然后在测试里随机注入看 Agent 能不能正确处理。这个模块帮我发现了不少隐藏的 bug。7.6 性能优化要抓大头Agent 的性能瓶颈通常在模型调用上而不是在 Harness 本身。所以优化要从模型调用入手能并行的工具调用并行、能缓存的模型响应缓存、能精简的上下文精简。Harness 本身的优化空间不大不要在这里花太多时间。我实测过一个案例把工具调用从串行改成并行整体延迟降低了百分之六十。而优化 Harness 代码延迟只降低了百分之五。所以抓大头很重要。7.7 安全护栏不能省安全护栏包括输入过滤、输出过滤、工具权限、敏感操作审批。这些在 Demo 阶段可以省但上生产必须加。输入过滤防提示词注入输出过滤防敏感信息泄露工具权限防越权操作敏感操作审批防误操作。这四道防线缺一不可。我见过因为没加工具权限模型被诱导调用了删除数据的工具造成不可逆损失的案例。所以安全护栏是底线不能省。8. 后续扩展方向这套 Harness 搭好之后还有几个方向可以继续扩展。多 Agent 协作是一个把多个 Agent 组织成团队各司其职通过消息路由协作完成任务。自适应调度是另一个根据任务类型动态选择模型和策略简单任务用便宜模型复杂任务用强模型。持续学习也是一个方向把每次执行的成功和失败案例收集起来用于优化提示词和工具描述。但我要提醒一句扩展要按需来不要为了扩展而扩展。我见过太多项目核心功能还没跑通就开始搞多 Agent 协作结果复杂度失控项目黄了。先把单 Agent 跑稳再考虑扩展这是更稳妥的路径。我个人在实际操作中的体会是Agent 架构选型没有标准答案只有适合当前场景的答案。从一次 LLM 调用起步遇到问题就往上加一层加到问题解决为止这是最务实的做法。不要一上来就追求完整 Harness Runtime也不要永远停在裸调用。架构是演进来的不是设计出来的。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑