DeepAgents Harness位置解析:Agent架构中的关键决策点 1. 从“Harness 位置”说起这个笔记到底在记什么第一次看到“DeepAgents笔记——Harness 位置”这个标题很多人会愣一下Harness 是什么位置又指什么其实这是在做 DeepAgents 这类 Agent 框架时一个非常具体、非常容易踩坑的问题——Harness 这个组件在整个 Agent 运行链路里到底应该放在哪一层。它不是一个抽象概念而是一个决定你的 Agent 能不能稳定跑起来、工具调用会不会串味、状态管理会不会崩掉的关键架构决策。先把话说白DeepAgents 是建立在 LangChain 和 LangGraph 之上的一套 Agent 构建范式它把“规划、执行、反思、工具调用”这些环节拆成了可组合的节点。而 Harness 在这个语境里通常指的是包裹在模型或工具外面的一层“执行外壳”——它负责把原始输入整理成模型能吃的格式、把模型吐出来的东西解析成结构化动作、再决定下一步交给谁。你可以把它理解成 Agent 的“驾驶舱”模型是发动机工具是轮子Harness 就是方向盘和仪表盘之间的那套传动机构。这篇笔记要解决的问题很实在很多人在用 DeepAgents 搭 Agent 时Harness 放错位置导致工具调用参数丢失、多轮对话状态错乱、甚至出现“模型说调用了工具但实际没调”的假动作。适合谁看如果你正在用 LangGraph 做 Agent 编排或者刚接触 LangChain 的 DeepAgents 想搞清楚内部结构再或者你已经被“harness failed to load plugins”这类报错折磨过那这篇内容就是写给你的。接下来我会把 Harness 的位置问题拆成设计思路、核心细节、实操落地、问题排查四个层面全部基于实际项目经验来讲不绕弯子。2. 整体设计与思路拆解Harness 为什么不能随便放2.1 Harness 在 Agent 链路中的三种典型位置在 DeepAgents 的架构里Harness 的位置选择本质上是在回答一个问题“谁负责把模型的意图翻译成可执行的动作”。根据我实际搭过的几个项目Harness 通常出现在三个位置每个位置对应不同的职责边界。第一种是模型前置 Harness放在 LLM 调用之前。它的工作是拼 prompt、注入工具描述、管理上下文窗口。这种位置的好处是模型看到的东西是经过清洗的不会因为历史消息里混入了工具返回的原始 JSON 而干扰判断。坏处是它离工具执行很远如果工具返回格式变了前置 Harness 感知不到。第二种是工具后置 Harness放在工具执行之后、结果回传给模型之前。它负责把工具返回的原始数据比如数据库查询结果、API 响应转成模型能理解的摘要或结构化信息。这个位置在 RAG 流程里特别常见因为检索回来的文档往往又长又杂不加工直接塞给模型就是浪费 token。第三种是循环中枢 Harness放在 LangGraph 的状态图中间作为路由节点存在。它既看模型输出也看工具结果决定下一步是继续调工具、还是结束、还是走反思分支。这种位置最灵活但也最容易写成一团乱麻因为所有逻辑都堆在一个节点里。提示三种位置不是互斥的成熟项目里往往是“前置 中枢”组合后置 Harness 根据工具类型选择性使用。2.2 为什么位置选错会出大问题我踩过最典型的一个坑把工具参数解析逻辑放在了前置 Harness 里。当时想的是“提前把参数准备好模型直接填就行”结果模型在多轮对话中改了主意想调另一个工具但前置 Harness 已经按旧工具的参数模板把输入锁死了导致模型输出的参数对不上工具调用直接失败。这就是典型的位置与职责错配——参数解析应该发生在模型输出之后而不是之前。另一个常见问题是状态污染。LangGraph 的状态是全局共享的如果 Harness 放在中枢位置却直接修改了 messages 数组而没有通过 reducer 函数那么并发分支执行时就会出现状态覆盖。我见过一个项目两个工具并行调用Harness 在中枢节点里把两个结果都写进了同一个 state key后写的把先写的覆盖了模型最后只看到一个工具的结果还以为是另一个工具没返回。所以位置选择的核心原则是Harness 应该放在“信息形态发生转换”的边界上。模型输出是自然语言工具输入是结构化参数这个转换边界就是 Harness 该待的地方。工具输出是原始数据模型输入是上下文这个转换边界也是 Harness 该待的地方。把 Harness 放在边界上它才有明确的输入输出契约才不会越界去干别的活。2.3 与 LangGraph 节点设计的对应关系LangGraph 的节点本质上是函数输入是 state输出是 state 的增量。Harness 如果作为一个节点存在它的函数签名就决定了它的位置。我习惯把 Harness 设计成纯函数节点输入 state输出一个包含next_action和action_input的字典不直接执行工具也不直接修改 messages。这样 Harness 的位置就固定在“模型节点之后、工具节点之前”职责单一测试也方便。如果你把 Harness 写成带副作用的节点比如在里面直接调了外部 API那它的位置就变得模糊了——它既是路由器又是执行器出了问题你都不知道该查模型还是查 Harness。我在实际项目中坚持一个原则Harness 只做决策和格式转换不做 IO。IO 交给专门的工具节点这样 Harness 的位置永远清晰。3. 核心细节解析与实操要点Harness 位置相关的关键参数3.1 状态字段设计决定 Harness 能放在哪LangGraph 的 state 定义直接限制了 Harness 的位置。如果你的 state 里只有messages一个字段那 Harness 只能围绕 messages 做文章位置就被限制在消息处理链上。但如果你定义了pending_tool_calls、tool_results、scratchpad这些字段Harness 就可以在更丰富的信息面上做决策。我一般会定义这样的 state 结构from typing import TypedDict, Annotated from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] pending_tool_calls: list tool_results: dict harness_decision: str iteration_count: int这里pending_tool_calls是 Harness 的输出tool_results是工具节点的输出harness_decision是 Harness 告诉图下一步往哪走。有了这些字段Harness 的位置就固定在“读 messages 和 tool_results写 pending_tool_calls 和 harness_decision”这个环节。字段设计清楚了位置就不会乱。注意add_messages这个 reducer 很关键它保证消息是追加而不是覆盖。如果你自己写 reducer 却忘了处理并发Harness 在中枢位置读到的 messages 可能是不完整的。3.2 工具描述注入的位置影响调用准确率Harness 前置的时候工具描述是在拼 prompt 阶段注入的。这里有个细节工具描述注入的位置越靠近用户消息模型越容易正确调用。我实测过把工具 schema 放在 system message 最前面和放在用户消息紧邻的位置调用准确率能差 15% 左右。原因是长上下文里模型对首尾信息的注意力更强工具描述如果被埋在中间容易被忽略。所以如果你的 Harness 是前置的建议把工具描述拼在 system message 的末尾或者单独作为一条 system 消息紧跟在用户消息之前。LangChain 的bind_tools方法内部就是这么做的它会把工具 schema 转成模型特定的格式放在合适的位置。如果你自己手写 Harness千万别把工具描述随便塞。3.3 多轮对话中 Harness 的状态清理时机多轮对话是 Harness 位置问题的高发区。每一轮用户输入进来Harness 都要决定上一轮的pending_tool_calls要不要清空tool_results要不要保留我的经验是Harness 在每轮开始时应该重置pending_tool_calls和harness_decision但保留tool_results和messages。因为工具结果可能还需要被后续轮次引用而待执行动作和决策是瞬时的。如果你不清理pending_tool_calls会出现上一轮没执行完的工具调用被这一轮重复执行的情况。我见过一个项目用户问了个新问题结果 Agent 把上一轮已经查过的数据库又查了一遍就是因为 Harness 没在轮次边界清理状态。清理时机应该放在 Harness 节点的入口处而不是出口处这样即使上一轮异常中断这一轮也能从干净状态开始。3.4 Harness 与中间件的协作边界LangChain 的 Agent 中间件middleware和 Harness 容易职责重叠。中间件通常做的是横切关注点比如日志、限流、重试Harness 做的是纵向的决策和转换。我的划分原则是中间件不碰 state 的业务字段Harness 不碰 IO 和重试。如果你把重试逻辑写进 Harness那 Harness 的位置就变成了“执行器 决策器”一旦重试次数多了Harness 的决策逻辑会被重试状态干扰。实际项目中我会在 Harness 外面包一层中间件做重试Harness 内部只关心“这次模型输出该走哪个分支”。这样 Harness 的位置始终在决策层不会被执行层的细节污染。4. 实操过程与核心环节实现把 Harness 放到正确位置4.1 搭建最小可运行的 DeepAgents 骨架先搭一个最小骨架把 Harness 的位置显式标出来。我用 LangGraph 的StateGraph来组织节点包括model、harness、tools、reflect。Harness 放在model和tools之间这是最经典的位置。from langgraph.graph import StateGraph, END from langchain_openai import ChatOpenAI from langchain_core.tools import tool tool def search_database(query: str) - str: 根据关键词查询数据库 return f查询结果{query} 的相关数据 tools [search_database] llm ChatOpenAI(modelgpt-4o).bind_tools(tools) def model_node(state: AgentState): response llm.invoke(state[messages]) return {messages: [response]} def harness_node(state: AgentState): last_message state[messages][-1] if hasattr(last_message, tool_calls) and last_message.tool_calls: return { pending_tool_calls: last_message.tool_calls, harness_decision: call_tool } return {harness_decision: end} def tool_node(state: AgentState): results {} for call in state[pending_tool_calls]: tool_fn {t.name: t for t in tools}[call[name]] results[call[id]] tool_fn.invoke(call[args]) return {tool_results: results, pending_tool_calls: []} graph StateGraph(AgentState) graph.add_node(model, model_node) graph.add_node(harness, harness_node) graph.add_node(tools, tool_node) graph.set_entry_point(model) graph.add_edge(model, harness) graph.add_conditional_edges(harness, lambda s: s[harness_decision], { call_tool: tools, end: END }) graph.add_edge(tools, model) app graph.compile()这段代码里Harness 的位置非常明确它在model之后读messages的最后一条判断有没有tool_calls有就写pending_tool_calls并路由到tools没有就结束。它不执行工具不修改 messages只做决策。这就是我推荐的 Harness 位置。4.2 参数计算Harness 决策的阈值怎么定Harness 在决定“是否继续调工具”时往往需要一个迭代次数上限防止无限循环。这个阈值怎么定我的经验公式是最大迭代次数 预期工具调用链长度 × 1.5 2。比如一个查询任务通常需要 2 次工具调用先查 ID 再查详情那阈值设 5 比较合适。设太小会提前截断设太大浪费 token。在 Harness 里加这个判断MAX_ITERATIONS 5 def harness_node(state: AgentState): if state.get(iteration_count, 0) MAX_ITERATIONS: return {harness_decision: end, messages: [ {role: assistant, content: 已达到最大工具调用次数停止执行。} ]} last_message state[messages][-1] if hasattr(last_message, tool_calls) and last_message.tool_calls: return { pending_tool_calls: last_message.tool_calls, harness_decision: call_tool, iteration_count: state.get(iteration_count, 0) 1 } return {harness_decision: end}这里iteration_count在 Harness 里递增而不是在工具节点里因为 Harness 是决策点它最清楚“这是第几次决策”。工具节点只负责执行不关心轮次。4.3 实操现场一次工具调用失败的完整排查记录有一次线上 Agent 突然开始报“工具参数缺失”我按 Harness 位置逐层排查。先看 Harness 输出的pending_tool_calls发现args是空的。再看模型原始输出tool_calls里其实有参数但参数是嵌套在function.arguments里的 JSON 字符串。问题出在 Harness 解析时直接取了call[args]而 LangChain 某些版本返回的结构是call[function][arguments]。修复方式是在 Harness 里做兼容解析def normalize_tool_call(call): if args in call: return call if function in call: import json return { id: call.get(id), name: call[function][name], args: json.loads(call[function][arguments]) } raise ValueError(f无法解析的工具调用格式{call})这个坑的根源是 Harness 位置太靠近模型输出直接吃了原始格式。如果 Harness 放在更靠后的位置比如工具节点内部再解析问题可能不会暴露得这么早但会更难排查。所以 Harness 靠近模型输出有好处格式问题早发现早修复。4.4 并发场景下 Harness 的位置调整当 Agent 需要并行调用多个工具时Harness 的位置要稍微后移。因为并行工具调用会产生多个结果Harness 如果还在“模型之后、工具之前”它只能看到模型的调用请求看不到并行执行的结果汇总。这时候我会在工具节点之后再加一个 Harness 节点专门做结果聚合和下一轮决策。结构变成model - harness_pre - tools_parallel - harness_post - model。harness_pre负责拆分并行调用harness_post负责合并结果并判断是否继续。两个 Harness 位置不同职责也不同。harness_pre的输出是pending_tool_calls列表harness_post的输出是harness_decision和更新后的messages。提示并行工具调用时tool_results要用字典按tool_call_id索引不要用列表否则结果顺序和调用顺序对不上Harness 聚合时会错位。5. 常见问题与排查技巧实录Harness 位置相关的坑5.1 Harness 位置问题速查表现象可能的位置问题排查方法修复方式工具调用参数为空Harness 解析格式不兼容打印模型原始输出加格式兼容层工具重复执行Harness 未清理 pending_tool_calls检查轮次入口状态入口处重置字段状态被覆盖Harness 直接改 messages 未用 reducer检查 state 定义使用 add_messages无限循环调用Harness 缺少迭代上限打印 iteration_count加阈值判断并行结果错位Harness 用列表存结果检查 tool_results 结构改用字典按 id 索引模型看不到工具结果Harness 位置太靠后检查 messages 更新时机工具结果及时回写 messages这张表是我从三个项目里攒出来的基本覆盖了 Harness 位置相关的八成问题。遇到报错先对表能省不少时间。5.2 “harness failed to load plugins” 的排查思路这个报错在热词里出现过虽然具体插件机制因项目而异但排查思路是通用的。首先确认 Harness 的初始化位置——如果 Harness 是在图编译之后才加载插件而插件又依赖图里的节点信息就会加载失败。正确顺序是先注册插件再编译图最后运行。我见过有人把插件加载写在app.invoke()之后那当然加载不了。其次检查插件的路径解析。Harness 如果放在子目录里插件的相对路径基准会变。用绝对路径或者基于__file__的动态路径最稳。最后看插件之间的依赖顺序有些插件依赖另一个插件注册的 state 字段顺序反了就会失败。我的做法是在 Harness 初始化时打印插件加载日志每个插件加载成功或失败都记一行排查时一目了然。5.3 Harness 决策与模型输出不一致怎么办有时候模型明明说了“我要调用搜索工具”但 Harness 判断出来是“结束”。这种不一致通常是 Harness 的解析逻辑太严格。比如模型输出的是tool_calls为空但 content 里写了“我将调用搜索”Harness 只看tool_calls就会误判。我的处理方式是Harness 优先看结构化字段结构化字段为空时再用正则从 content 里兜底提取。import re def harness_node(state: AgentState): last_message state[messages][-1] if hasattr(last_message, tool_calls) and last_message.tool_calls: return {pending_tool_calls: last_message.tool_calls, harness_decision: call_tool} content getattr(last_message, content, ) if re.search(r调用|执行|查询, content) and 无法 not in content: return {harness_decision: retry_model} return {harness_decision: end}retry_model是一个回退分支让模型重新输出结构化调用。这个兜底逻辑救过我好几次尤其是用一些对工具调用支持不太好的模型时。5.4 独家避坑Harness 日志要记什么Harness 是决策点日志必须记全。我固定记四样东西输入 messages 的最后一条、输出的 harness_decision、pending_tool_calls 的 id 列表、当前 iteration_count。这四样能还原每一次决策的上下文。不要记完整 messages太长也不要只记 decision信息不够。日志格式用 JSON 一行一条方便后续用脚本分析。我有个项目就是靠分析 Harness 日志发现某个工具在特定输入下总是返回空从而定位到工具本身的 bug。Harness 日志是 Agent 可观测性的第一道防线位置越核心日志越要细。6. 关于 Harness 位置我个人的几条经验Harness 的位置没有银弹但有一条判断标准很实用如果 Harness 的输入输出能用一句话说清楚位置就对了。比如“输入模型消息输出下一步动作”这就是清晰的位置。如果说不清楚比如“输入消息输出动作顺便还改了数据库”那位置肯定有问题。另外Harness 尽量保持无状态。所有需要跨轮次保留的信息都放 state 里Harness 每次执行都从 state 读、往 state 写自己不存任何东西。这样 Harness 的位置可以随时调整今天放在模型后面明天想放到工具后面改一下图的边就行不用改 Harness 内部逻辑。最后分享一个小技巧调试 Harness 位置时先把 Harness 做成一个独立的可测试函数用假的 state 喂给它看输出对不对。确认逻辑没问题再放进图里。这样能把“位置问题”和“逻辑问题”分开排查效率高很多。我在实际项目里就是这么干的Harness 的单元测试覆盖率永远是最高的因为它最值得测。