
1. 先搞清楚 Harness 到底指什么很多人第一次听到 AI Agent 的 Harness脑子里浮现的是测试框架或者某种脚手架工具。这个理解不算错但太窄了。在 AI Agent 的语境里Harness 指的是包裹在 LLM 外面、让模型真正能干活的那一整套运行时基础设施。模型本身只会输出 token它不会读文件、不会发请求、不会记住上一轮对话、更不会在失败后重试。把这些能力补齐的那层东西就是 Harness。你可以把 LLM 想象成一个极其聪明但被关在隔音玻璃房里的专家。他能回答问题但看不见外面的世界也伸不出手。Harness 就是给这位专家配的电话线、机械臂、记事本和助手团队。没有 Harness再强的模型也只是一个聊天框有了 Harness它才能变成能自主完成任务的 Agent。我最初接触这个概念时也走过弯路以为接个 API、写个 while 循环就算搭好 Agent 了。结果一上真实任务就崩工具调用格式解析失败、上下文超长被截断、循环跑飞停不下来、并发一上来就乱序。后来才明白Agent 的工程质量几乎全部落在 Harness 上而不是模型本身。模型是买来的Harness 才是你自己要造的东西。那 Harness 到底由哪些部分组成拆到最细没必要但归纳到可落地的粒度核心就是七个子系统。下面我按从请求进来到结果出去的实际数据流把这七块逐一拆开讲每一块都配上我踩过的坑和可复现的做法。2. 七个子系统的整体架构与数据流2.1 为什么是七块而不是三块或十块先说拆分的逻辑。一个 Agent 跑一次任务本质上要回答七个问题模型从哪来LLM Integration、这一轮该带什么上下文Context Management、模型说要调工具怎么执行Tool Execution、多轮怎么串起来Agent Loop、状态存哪Memory State、同时来一堆任务怎么办Concurrency Scheduling、出错了怎么发现和恢复Observability Recovery。这七个问题各自独立、职责清晰任何一块缺失都会导致系统在真实场景下不可用。拆成三块模型、工具、循环会漏掉状态、并发和可观测性这三块恰恰是 demo 和生产的最大分水岭。拆成十块以上又会过度设计小团队根本维护不过来。七块是我实践下来既能覆盖生产需求、又不至于让个人开发者望而却步的平衡点。2.2 一次完整请求的数据流把七块串起来看一次 Agent 任务的流转是这样的用户输入进入Agent LoopLoop 初始化本轮状态Loop 向Context Management要这一轮该发给模型的完整 promptContext 从Memory State取出历史、从工具注册表取出可用工具描述拼装成消息拼好的请求交给LLM Integration由它处理鉴权、重试、流式解析模型返回内容LLM Integration解析出是普通回复还是工具调用如果是工具调用交给Tool Execution执行结果写回Memory StateLoop 判断是否继续下一轮Concurrency Scheduling决定这个任务和其他任务怎么排队全程Observability Recovery记录每一步出错时触发重试或降级这个数据流是理解后面所有细节的主线。你会发现Loop 是骨架其他六块是挂在骨架上的器官。下面逐块拆。3. 子系统一LLM Integration模型接入层3.1 它到底要解决什么问题很多人觉得调模型就是发个 HTTP 请求能有多难。真做过就知道模型接入层要处理的破事一大堆不同厂商的 API 格式不一样、流式和非流式返回结构不同、工具调用的 JSON 可能不合法、限流和超时随时发生、token 计费要统计、多模型要能热切换。这一层做不好上层逻辑写得再漂亮也是空中楼阁。我的做法是在这一层做厚把脏活全吃掉让上层只面对一个干净的接口。上层调用时只说给我一个回复可能带工具调用至于底层是哪个模型、怎么重试、怎么解析上层完全不关心。3.2 统一抽象与多模型适配核心是定义一个统一的请求/响应结构。请求侧统一成 messages 数组加 tools 列表响应侧统一成 content 加 tool_calls。不同厂商的差异在适配器里消化掉。class LLMResponse: def __init__(self, content, tool_calls, usage, finish_reason): self.content content self.tool_calls tool_calls # 统一成 [{id, name, arguments}] self.usage usage self.finish_reason finish_reason class BaseAdapter: def chat(self, messages, tools, streamFalse): raise NotImplementedError class OpenAICompatAdapter(BaseAdapter): def chat(self, messages, tools, streamFalse): # 处理 OpenAI 兼容格式含流式增量拼接 ...这样设计的好处是换模型只改适配器Agent Loop 一行不动。我实测过从一家模型切到另一家只要适配器写对上层逻辑零改动。3.3 流式解析与工具调用拼接流式返回是坑最多的地方。模型返回工具调用时arguments 是分片吐出来的你必须自己拼接完整再解析 JSON。我见过太多人直接对每个 chunk 做 json.loads结果必然报错。正确做法是维护一个按 index 索引的缓冲区把每个 delta 的 arguments 片段累加等 finish_reason 变成 tool_calls 时再统一解析。这里还要处理一个恶心情况模型偶尔吐出不合法 JSON比如多一个逗号、少一个引号。我的经验是加一层容错解析失败时尝试修复常见错误再失败就带着错误信息让模型重试一次。注意流式场景下不要在每个 chunk 都触发 UI 更新和计费那样既卡又乱。按 token 累积到一定量或按时间窗口批量刷新体验和性能都更好。3.4 重试、限流与成本控制重试要区分错误类型。网络超时、5xx 可以指数退避重试4xx 里的鉴权失败、参数错误重试没意义直接抛出。限流429要读响应头里的重试时间别傻等固定间隔。成本控制这块我习惯在适配器里记录每次调用的 token 数和估算费用按任务维度汇总。这样跑一段时间就能看出哪个环节最烧钱往往是上下文太长或者循环次数太多。没有成本可观测性的 Agent跑着跑着账单就失控了。4. 子系统二Context Management上下文管理4.1 上下文是 Agent 最稀缺的资源模型的上下文窗口再大也是有限的而 Agent 跑多轮任务时历史消息、工具返回结果、系统提示会迅速膨胀。上下文管理要解决的核心矛盾是既要给模型足够的信息做决策又不能让 prompt 无限增长导致超限或成本爆炸。我见过最典型的翻车场景一个 Agent 处理长文档把整篇文档塞进上下文第一轮还行跑到第五轮直接超限报错。这不是模型的问题是上下文管理没做好。4.2 分层组织与优先级我的做法是把上下文分成几层按优先级动态裁剪层级内容是否可裁剪裁剪策略系统层角色设定、核心规则不可裁剪永远保留任务层当前任务目标、约束尽量保留压缩为摘要工具层可用工具描述可裁剪按需注入历史层对话与工具结果可裁剪摘要或滑窗即时层最近一轮交互不可裁剪永远保留系统层和即时层是硬约束中间三层按 token 预算动态调整。这样即使任务跑很久核心信息也不会丢。4.3 摘要压缩与滑窗的取舍历史太长时有两个选择滑窗只保留最近 N 轮和摘要把旧内容压缩成一段话。滑窗简单但会丢信息摘要保留信息但可能失真。我的经验是两者结合最近几轮用原文保留细节更早的内容用摘要压缩。摘要的 prompt 要明确要求保留关键决策、已完成的步骤、未解决的问题而不是泛泛地总结。实测下来这种混合策略在长任务上的表现明显好于纯滑窗。提示摘要本身也是一次 LLM 调用有成本和延迟。不要每轮都摘要可以设定阈值比如历史超过窗口的 60% 才触发一次压缩。4.4 工具描述的按需注入工具多了以后把所有工具描述都塞进 prompt 会占用大量 token还会干扰模型选择。我的做法是按任务类型动态注入相关工具。比如任务是查数据就只注入数据库查询类工具不注入发邮件、写文件的工具。这样既省 token又提高工具选择的准确率。5. 子系统三Tool Execution工具执行层5.1 工具是 Agent 的手脚模型再聪明不能执行就等于零。工具执行层负责把模型输出的我要调用某工具、参数是这些变成真实的动作再把结果返回给模型。这一层的关键词是安全、可靠、可观测。5.2 工具注册与 Schema 定义每个工具要有清晰的名称、描述和参数 schema。描述写得好不好直接决定模型会不会正确使用。我踩过的坑是描述写得太简略模型经常传错参数类型。后来我把描述写得像给新人看的文档包含用途、参数含义、示例工具调用准确率明显提升。tools [ { name: query_database, description: 根据 SQL 查询数据库并返回结果。仅支持 SELECT 语句。, parameters: { type: object, properties: { sql: {type: string, description: 标准 SQL 查询语句}, limit: {type: integer, description: 返回行数上限默认 100} }, required: [sql] } } ]5.3 参数校验与沙箱隔离模型给的参数永远不要直接信任。必须做类型校验、范围校验、白名单校验。执行 SQL 要限制只能 SELECT执行 shell 要限制命令白名单访问文件要限制目录范围。沙箱隔离是底线。我习惯把工具执行放在受限环境里超时强制中断资源用量设上限。曾经有个 Agent 因为工具里写了个死循环把整个进程拖死从那以后所有工具执行都加了超时。5.4 执行结果的处理与回填工具返回的结果可能很长直接塞回上下文会撑爆窗口。我的做法是结果先截断或摘要再回填。比如查询返回一千行只把前若干行和总行数给模型需要更多再让它分页查。结果回填的格式也要统一明确标注是哪个工具、调用是否成功、返回了什么。这样模型下一轮才能正确理解。注意工具执行失败时不要把原始堆栈直接给模型那会污染上下文。转成人类可读的错误描述比如查询失败字段名不存在让模型有机会修正。6. 子系统四Agent Loop智能体主循环6.1 Loop 是整个 Harness 的心脏前面三块都是为 Loop 服务的。Loop 负责编排拿上下文、调模型、判断要不要执行工具、执行完再回到模型、直到任务完成或达到终止条件。这个循环写得好不好决定了 Agent 是能干活还是瞎折腾。6.2 循环的终止条件设计最常见的 bug 是循环停不下来。模型一直调工具或者一直说我再想想跑几十轮还在原地。必须设计多重终止条件模型返回了最终答案没有工具调用达到最大轮数上限比如 15 轮连续 N 轮没有实质性进展总 token 或总耗时超预算检测到重复的工具调用模式我一般把最大轮数设成 10 到 15配合无进展检测。无进展的判定可以看连续几轮的工具调用是否高度相似或者模型输出是否在重复。6.3 单轮循环的完整实现def run_agent(task, max_turns15): state init_state(task) for turn in range(max_turns): context build_context(state) response llm.chat(context, toolsregistry.tools) if not response.tool_calls: return response.content # 任务完成 for call in response.tool_calls: result execute_tool(call, sandboxTrue, timeout30) state.add_tool_result(call.id, result) if no_progress(state): return 任务未能推进已停止 return 达到最大轮数已停止这段代码看着简单但每一行背后都有讲究。build_context 要做上下文裁剪execute_tool 要做校验和隔离no_progress 要做模式检测。6.4 循环中的状态传递每一轮之间要传递什么状态我的经验是至少包含任务目标、已完成的步骤、当前待解决的问题、工具调用历史。这些状态既影响上下文构建也影响终止判断。状态设计得清晰调试时一眼就能看出 Agent 卡在哪。7. 子系统五Memory State记忆与状态7.1 短期记忆与长期记忆的分工短期记忆是当前任务内的对话和工具结果任务结束就丢弃。长期记忆是跨任务的知识比如用户偏好、历史结论、领域知识。两者存储方式和生命周期完全不同不能混在一起。短期记忆我一般放内存或 Redis读写快、过期自动清理。长期记忆放向量库或关系库需要检索时再取。7.2 状态持久化与断点续跑Agent 任务可能跑很久中途进程挂了怎么办状态持久化就是答案。每一轮结束把状态存下来重启后能从断点继续。这在长任务场景下是刚需。我踩过的坑是状态序列化时把不可序列化的对象也存了恢复时报错。后来规定状态里只放基础类型和明确可序列化的结构问题就没了。7.3 记忆检索的时机与策略长期记忆不是每轮都检索那样又慢又费 token。我的做法是在任务开始时检索一次相关背景任务过程中如果模型明确需要历史信息再触发检索。检索结果也要做相关性过滤别把不相关的记忆塞进去干扰模型。8. 子系统六Concurrency Scheduling并发与调度8.1 并发是 Agent 从玩具到生产的分水岭单用户单任务时怎么写都行。一旦多个用户同时用或者一个任务要并行处理多个子任务并发问题就全冒出来了。上下文串了、状态覆盖了、限流打爆了这些都是并发没做好。8.2 任务队列与隔离我的做法是每个任务有独立的会话 ID 和状态空间任务之间完全隔离。任务进队列由 worker 池消费。这样既能控制并发数又能保证隔离性。class TaskScheduler: def __init__(self, max_workers10): self.queue asyncio.Queue() self.semaphore asyncio.Semaphore(max_workers) async def submit(self, task): await self.queue.put(task) async def worker(self): while True: task await self.queue.get() async with self.semaphore: await self.run_task(task)信号量控制并发上限避免把下游模型 API 打爆。队列保证任务不丢。8.3 限流与背压下游模型有 QPS 限制工具执行有资源限制这些都要在调度层做背压。当队列积压超过阈值要么拒绝新任务要么降级处理。我一般会监控队列长度和任务等待时间超过阈值就告警。8.4 并行工具调用的处理模型一轮可能返回多个工具调用这些调用如果互不依赖可以并行执行省时间。但要注意并行执行的结果回填顺序要和调用顺序对应否则模型会混乱。我的做法是并行执行、按原顺序回填。提示并行工具调用要设总超时不能因为一个慢工具拖垮整轮。用 asyncio.gather 配合 timeout超时的工具返回超时错误让模型决定是否重试。9. 子系统七Observability Recovery可观测与恢复9.1 看不见的 Agent 等于失控Agent 跑起来是个黑盒如果不记录每一步出问题根本没法查。可观测性要覆盖每轮的输入输出、工具调用及结果、token 消耗、耗时、错误。这些数据既是调试依据也是优化依据。9.2 结构化日志与链路追踪我习惯给每个任务分配 trace_id所有日志带上这个 ID这样能完整还原一个任务的全过程。日志要结构化方便检索和统计。logger.info(tool_call, extra{ trace_id: state.trace_id, turn: turn, tool: call.name, args: call.arguments, duration_ms: elapsed, success: result.success })9.3 错误分类与恢复策略错误要分类处理模型调用失败可重试工具执行失败可让模型换方案上下文超限要触发压缩循环卡死要强制终止。每类错误对应不同的恢复动作不能一刀切。错误类型典型场景恢复策略模型超时/限流API 不稳定指数退避重试工具参数错误模型传错参数返回错误让模型修正上下文超限历史过长触发摘要压缩循环无进展模型原地打转强制终止并报告状态损坏序列化异常从上一个检查点恢复9.4 人工介入与降级有些情况 Agent 自己解决不了需要人工介入。比如连续失败、涉及敏感操作、置信度低。这时候要能暂停任务、通知人工、支持人工修正后继续。降级策略也要有比如模型不可用时切换到备用模型或返回兜底回复。10. 常见问题与排查技巧实录10.1 工具调用格式解析失败这是最高频的问题。模型返回的 arguments 不是合法 JSON或者流式拼接时漏了片段。排查思路先打印原始返回确认是模型问题还是解析问题。如果是模型问题在 prompt 里强调必须返回合法 JSON或者用支持结构化输出的模型。如果是解析问题检查流式拼接逻辑确保按 index 正确累加。10.2 循环停不下来先看日志里每轮的工具调用判断是模型在重复调用还是真的在推进。如果是重复检查工具返回结果是否让模型误以为没成功。如果是推进但慢调大最大轮数或优化上下文。我遇到过一次是工具返回格式不清晰模型以为失败了反复重试改清楚返回格式就好了。10.3 上下文超限监控每轮的 token 数找出增长最快的部分。通常是工具返回结果太长。解决方法是结果截断加摘要或者分页返回。另外检查系统提示是不是写得太长有些人的系统提示能写几千字纯属浪费。10.4 并发下状态串扰症状是 A 用户的任务里出现了 B 用户的数据。排查方向检查状态是否用了全局变量检查会话 ID 是否正确隔离检查异步任务是否共享了可变对象。我踩过一次是用了类变量存状态多任务一跑就串改成实例变量就好了。10.5 成本失控按任务统计 token 消耗找出最烧钱的环节。常见原因是上下文太长、循环轮数太多、工具返回结果太大。针对性优化压缩上下文、设轮数上限、截断工具结果。我一般会设一个单任务成本上限超了就终止。提示排查问题时先把 trace 日志拉出来完整看一遍90% 的问题看日志就能定位别急着改代码瞎猜。11. 从零搭一个最小可用 Harness 的实操顺序如果你现在就要动手我建议按这个顺序来每一步都能跑通再进下一步先写 LLM Integration能调通一个模型能解析普通回复和工具调用能处理流式。这一步跑通你就有了最基础的对话能力。加 Tool Execution注册一两个简单工具能执行、能回填结果。这一步跑通模型就能动手了。写 Agent Loop把前两步串起来加上终止条件。这一步跑通一个能完成简单任务的 Agent 就成型了。补 Context Management加上上下文裁剪和摘要让长任务不超限。加 Memory State状态持久化支持断点续跑。上 Concurrency任务队列和隔离支持多任务。最后补 Observability日志、追踪、错误恢复。这个顺序的好处是每一步都有可验证的产出不会一开始就陷入架构泥潭。我见过太多人一上来就想把七块全搭好结果哪块都没跑通最后放弃。12. 几个容易被忽略的工程细节12.1 工具描述的质量决定一切模型选不选对工具、传不传对参数八成取决于工具描述写得好不好。把描述当成给新人的文档来写包含用途、参数、示例、边界情况。这一块的投入回报率极高。12.2 终止条件要冗余不要只靠一个终止条件。最大轮数、无进展检测、超时、超预算多重保险。我吃过亏只设了最大轮数结果模型每轮都调工具但没进展白白烧了十几轮的钱。12.3 状态设计要面向调试状态里存什么直接决定你调试时能看到什么。我习惯把每轮的决策依据、工具调用、结果都存进状态出问题时能完整还原。多存一点不亏调试时省的时间远超存储成本。12.4 错误信息要给人看也给模型看工具执行失败时返回给模型的错误信息要清晰可操作比如字段 user_id 不存在可用字段有 id、name、email这样模型下一轮就能修正。含糊的执行失败只会让模型瞎猜。12.5 别过早优化七个子系统不是一开始都要做到生产级。先让核心链路跑通再逐步加固。我见过有人花两周设计完美的并发架构结果单任务都还没跑通。先能干活再谈干得好。这套 Harness 的七个子系统说到底就是把让模型真正干活这件事拆解成可管理、可迭代的模块。模型会不断更新换代但 Harness 的这套骨架是稳定的。把骨架搭好换什么模型都能快速接上。我自己从最初一个 while 循环的玩具到现在能扛住多任务并发的系统中间踩的坑基本都在这七块里。希望这些经验能帮你少走点弯路把精力花在真正创造价值的地方。