news 2026/9/12 16:03:41

DeepAgent实战:理清LangChain、LangGraph与Harness的三层分工

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepAgent实战:理清LangChain、LangGraph与Harness的三层分工

这两天把 DeepAgent 相关的几个概念从里到外捋了一遍,发现最容易卡住的不是模型调用,而是 Harness、LangChain、LangGraph 这三层到底怎么分工。这篇教程就按我实际跑通一个企业级 Agent 的顺序来写,先说结论:DeepAgent 这类框架,核心不是“能调用大模型”,而是把大模型、工具、记忆、流程和工程控制绑在一起,变成一条可运行、可观测、可恢复的任务流。适合谁看?准备做 Agent 应用开发、刚接触 LangChain 和 LangGraph、或者已经被“Agent 跑起来容易、跑稳很难”折腾过的人。最值得关注的点,是搞清楚 Harness 在 Agent 里的真实作用,不要只盯着模型名称和 Prompt。

现在很多讨论把三个概念混在一起,结果一上来就陷入“该学 LangChain 还是 LangGraph”的争论。我的做法是先把它们放回各自的位置:LangChain 是一堆成熟组件的集合,LangGraph 是负责流程编排的图执行器,Harness 则是包裹在整个 Agent 外面的工程控制层。分开理解之后,再做企业级落地会轻松很多。

1. 先分清三层:Harness、LangChain、LangGraph 各管什么

1.1 LangChain:组件库,不是灵魂

LangChain 解决的是“和大模型交互时经常重复造轮子”的问题。它把模型封装、Prompt 模板、Tool 包装、文档加载、向量库对接、记忆管理这些常见零件做成了统一接口。比如你接一个 OpenAI 兼容接口,或者接一个本地模型服务,在 LangChain 里通常只需要换一个 ChatModel 类,后面调用逻辑不用大改。

很多教程容易把 LangChain 讲成一个巨大的框架,好像什么都能做。实际跑下来,LangChain 的核心价值是组件库和抽象层,它确实能减少重复代码,但不会替你解决所有业务问题。业务逻辑、工具边界、异常处理,还是要自己写。

经常有人问“LangChain 是不是过时了”。我的看法是,它作为组件层仍然有使用价值,但作用边界在变化。现在更主流的做法是:把 LangChain 里的模型调用、工具封装拿过来,把流程编排交给 LangGraph,而不是让 LangChain 的链式结构承担所有复杂控制逻辑。

顺便说一个容易混淆的点:LangChain、vLLM、PyTorch 不是同一层的东西。PyTorch 是底层深度学习框架,vLLM 是推理服务,LangChain 是应用层开发框架。选型时要看自己缺哪一层,而不是把它们放到一个维度里比优劣。

1.2 LangGraph:用图把流程固定下来

LangGraph 的核心思想很简单:把 Agent 的执行流程定义成一张图,图里有节点、边、条件路由。每个节点处理一段状态,边决定下一步走向,条件边按函数判断走哪个分支。

相比传统 Chain 的线性调用,LangGraph 的优点是循环、分支、多轮任务都能显式控制。一个 Agent 需要调用工具,工具返回后又把结果喂回模型,模型再决定继续调用还是结束。这个循环如果用 Python 手写 while 循环,短期能用,一旦加入多分支、记忆、失败重试、并发控制,代码会很快变得不可维护。LangGraph 把这些控制逻辑建模成图,状态一目了然。

需要强调一点:LangGraph 不是来替代 LangChain 的。它是编排层,内部节点仍然可以使用 LangChain 的模型封装和 Tool 定义。它更像把原本松散的组织方式重新变成一张结构化的路线图。准备 LangChain 面试或做技术选型时,这个区别要能讲清楚。

1.3 Harness:给 Agent 套上一件可控外壳

Harness 这个词直译是“线束”或“捆绑”,在 Agent 场景里指的是运行时的工程约束层。它的作用包括:输入校验、超时控制、重试策略、日志捕获、结果格式校验、敏感信息过滤、调用次数限制。目的只有一个:让 Agent 的运行过程可预测、可控制、可回到正常状态。

现在搜索引擎里能翻到大量叫“xxx harness”的项目,比如某个模型名加 harness,或者某类工具名加 harness。这些项目通常代表作者在一个特定运行环境里把控制逻辑做通了,但这不代表它能直接搬到你的生产环境。使用前先看它锁定了哪个模型、依赖哪些库、是否还在维护,尤其要区分它是模型部署工具还是 Agent 编排工具。

我一般会把 Harness 和 Agent 本体拆开想:Agent 本体负责“思考”和“调用”,Harness 负责“拦住明显错误”和“在出错时留下足够信息”。这种拆法在排查问题时非常好用,很多线上问题并不是模型不会答,而是外层控制没有做好。

分层主要职责常见替代或配套方案
LangChain模型封装、Tool 定义、Prompt 模板、文档处理直接写模型调用 SDK
LangGraph状态图编排、条件路由、循环、持久化记忆手写 while 循环、状态机
Harness超时、重试、日志、输入输出校验、限流Web 服务框架、外部任务队列

2. 环境准备和最小可运行 Demo

2.1 本地环境怎么搭

在动手写代码之前,先把环境准备好。我不建议一上来就搭全量的大模型本地部署环境,学习阶段最重要的是把框架逻辑跑通。

通用步骤是:

python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install "langchain" "langgraph" "langchain-openai"

这里的依赖版本没有固定值,不同时期安装到的版本可能不一样。建议安装后先确认 import 是否正常,再继续往下走。如果你准备接本地模型,可以确认一下本地推理服务是否暴露了 OpenAI 兼容接口;如果没有,也可以先不接真实模型,用一个返回固定文本的函数代替,把编排流程跑通后再换真实模型。

为什么一定要先确认接口兼容性?因为 LangChain 的 ChatModel 抽象,本质上都是标准化的invoke方法。只要你的模型来源能封装成这个接口,后面所有编排代码都不用动。

2.2 最小 Agent 示例

下面这一段是示意代码,重点是把“模型调用节点”的最小结构展示出来。不同版本 API 会有细节差异,落地时以你实际安装版本的官方文档为准。

from typing import TypedDict class AgentState(TypedDict): messages: list next_step: str def call_llm(state: AgentState): # 这里假设 llm 已经定义好 # llm = ChatOpenAI(model="your-model-name") response = llm.invoke(state["messages"]) return { "messages": state["messages"] + [response], "next_step": "end" }

注意这个示例还没有把真实模型调用写进去,只是告诉你每个节点要返回一份状态增量。后面我们会把call_llm注册到图里。

我习惯先跑通单节点,再看多节点。原因很简单:单节点只牵涉模型的输入输出,如果这里就报错,大概率是模型封装、API Key、网络地址或消息结构的问题;这些问题不解决,后面加多少节点都会叠加更多干扰项。

2.3 跑通后先记录四件事

第一次跑通 Demo 后,不要急着加功能。先把四个东西记下来:

  • 状态对象的结构:messages 里存的是什么类型,是字符串还是消息对象。
  • 每个节点的返回字段:哪些字段会被后续节点读取,哪些只是临时数据。
  • 单次调用的耗时和日志路径:日志是打到控制台还是文件,能否定位到具体节点。
  • 失败时的报错信息:报错发生在模型调用前还是节点返回后。

这四条信息是后续加 Harness 的输入来源。没有这些基线数据,后面出了问题只能靠猜。

3. 企业级 Agent 的四个核心机制

3.1 模型调用和工具调用

Agent 的价值一半在工具调用,一半在模型理解。模型负责判断“要不要调用工具、调用哪个工具、传什么参数”,工具负责真正执行外部操作。二者必须稳定结合,否则流程再好看也没用。

工具调用的稳定性主要看三点:

  • 参数绑定是否精确:模型生成的工具参数能不能被正确解析成 JSON,并匹配到函数入参。
  • 返回结果是否能被模型理解:工具输出如果太长、太乱,模型可能读不到关键信息。
  • 失败时是否有明确的错误信息:工具抛出的异常能不能转换成模型能理解的描述,而不是直接中断整个流程。

现在工具接入也在走向标准化,MCP 这类协议想解决的问题,就是让工具描述和调用方式更统一。对于刚开始做 Agent 的人来说,不要急着追每一个协议,先把一个普通函数的调用跑通,再考虑是否接入标准协议。

3.2 记忆与状态管理

多轮对话的 Agent 必须处理记忆问题。最直接的做法是把历史消息一起传给模型,但这种方式有上限:上下文窗口是有限的,消息一多,成本、延迟、准确率都会变差。

更稳妥的做法是分层管理:

  • 短期记忆:当前任务轮次内需要保留的消息和状态。
  • 长期记忆:需要持久化到外部存储的关键信息,比如用户偏好、任务结果、中间摘要。

LangGraph 里做记忆管理的常用方式,是把记忆字段放进 State,再通过 Checkpointer 或外部存储持久化。State 是图的全局状态,每个节点都能读,但最好只改自己负责的那一段字段。这样多轮对话时,Agent 能知道自己在第几轮、已经拿到什么结果,而不是每次从零开始。

3.3 任务规划与条件路由

早期 LangChain 里常见的 Agent 范式是 ReAct:模型思考一下该调用什么工具,调用完看结果,再思考下一步。这个模式在简单任务上够用,但在复杂任务里容易失控,模型可能反复调用同一个工具,或者在一个分支里越走越远。

LangGraph 的改进是让流程显式化。你可以单独设计一个“规划节点”,让模型把任务拆成步骤,再走条件边判断下一步执行哪个节点。比如一个节点负责生成 SQL,一个节点负责执行查询,一个节点负责解释结果。条件边根据当前状态决定走查询节点还是直接结束。

这么做不是限制模型能力,而是把不确定的控制流尽量变成确定的分支。线上排查时,只要看状态走到了哪个节点、条件边是怎么判断的,就能定位问题。

3.4 重试、超时与可观测性

这是 Harness 的核心控制项。模型调用、工具调用、外部接口都可能失败,不要把所有策略都塞进 Prompt。Prompt 负责引导模型行为,Harness 负责兜底。

实践中我会为每个调用环节单独配置:

  • 超时时间:避免一个工具或模型调用卡住整个流程。
  • 重试次数:对偶发网络问题有用,但不要无限重试。
  • 最大迭代轮数:防止模型循环调用工具。
  • 日志记录:记录每次调用的输入、输出、耗时、错误信息。

如果做接口,还要考虑并发数和限流。默认参数适合入门,但生产任务必须单独压测,找到当前机器和模型服务能扛住的并发上限。

4. LangGraph 编排实战:一个带条件分支的 Agent

4.1 定义状态和节点

下面这个例子展示一个很简单的流程:模型先判断是否需要调用工具,如果需要就进入工具节点,否则直接结束。代码是示意风格,具体的 API 以你安装的 LangGraph 版本为准。

from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END class AgentState(TypedDict): messages: list need_tool: bool def call_llm(state: AgentState): # 这里用 llm.invoke 拿到模型返回 # 根据返回内容判断 need_tool 是 True 还是 False new_message = {"role": "assistant", "content": "我需要查询订单状态"} return { "messages": state["messages"] + [new_message], "need_tool": True } def call_tool(state: AgentState): # 模拟调用外部工具 tool_result = {"role": "tool", "content": "订单已发货"} return { "messages": state["messages"] + [tool_result], "need_tool": False }

这里的关键点,是每个节点返回的字段都要能在 State 里找到对应定义。节点之间不直接互相调用,它们只负责更新状态。图会根据状态决定下一步走向。

4.2 构建图和条件边

def route_after_llm(state: AgentState): if state["need_tool"]: return "call_tool" return END graph = StateGraph(AgentState) graph.add_node("call_llm", call_llm) graph.add_node("call_tool", call_tool) graph.add_edge(START, "call_llm") graph.add_conditional_edges("call_llm", route_after_llm) graph.add_edge("call_tool", END)

条件边是 LangGraph 里最值得花时间理解的部分。route_after_llm函数读一下当前状态,返回下一个节点的名字。这里就有判断逻辑:工具是否需要被调用、当前任务是否已经完成、失败时是否要走兜底节点。

注意不同版本的 LangGraph 对add_conditional_edges的写法可能有调整,有的版本需要传映射字典,有的版本直接返回节点名即可。遇到报错时,第一件事不是改逻辑,而是去确认当前版本的 API 签名。

4.3 编译和调用

app = graph.compile() result = app.invoke({ "messages": [{"role": "user", "content": "我的订单到哪了?"}], "need_tool": False }) print(result["messages"])

调用compile()之后,图就变成一个可执行对象。第一次运行后,建议打印一下result的结构,确认messages里累积了哪些消息,need_tool是否按预期变化。

如果输出和预期不一致,不用急着改图。先在节点里加打印或者日志,看看每个节点返回的状态增量到底是什么。状态传递如果出错,问题往往出在某个节点的返回字段上,而不是图结构本身。

4.4 从单轮走向多轮

单轮跑通之后,再加多轮和持久化。LangGraph 里可以通过 Checkpointer 保存状态快照,让同一份图在不同会话之间恢复历史状态。具体做法是编译时传入一个持久化对象,调用时带上线程 ID 或会话 ID。

不要一上来就设计非常复杂的记忆系统。先把“状态能够保存、能够恢复”跑通,再考虑哪些字段该长期保存、哪些字段该清理。记忆设计过于复杂,很容易变成后续排查的负担。

5. 底层原理:为什么 Agent 会乱跑,怎么把它拉回来

5.1 状态传递的边界

LangGraph 的底层可以理解成一个图执行器:每次调用从 START 节点进入,按边走向下一个节点,每个节点更新 State,图再根据新状态决定是否继续。

状态传递看起来是自动的,但有个隐藏问题:如果某个节点返回的字段类型和图定义不一致,或者更新了未定义的字段,后续节点拿到的 State 可能就是脏数据。最常见的情况是 messages 列表里混入了字符串和消息对象,导致模型调用时直接报格式错误。

我的建议是,State 类型定义要尽量精简。只放流程真正需要的字段,不要图方便把所有临时变量都塞进去。临时计算放在节点内部,不放到全局 State。

5.2 工具调用的确定性和异常

工具调用一旦接入真实系统,问题会立刻变多。外部 API 可能超时,数据库可能连接失败,文件路径可能不存在,权限可能不足。很多看起来像模型问题的报错,实际是工具层抛出来的。

为了让流程可控,我一般会在工具层做三层保护:

  • 工具入口做参数校验:先检查必要字段是否传入。
  • 调用外部服务时设置超时:不把整个 Agent 的寿命绑定在一个网络请求上。
  • 工具异常统一转换为结构化错误:比如返回{"error": "database timeout"},而不是直接抛一个让模型看不明白的堆栈。

工具调用尽量保持幂等。同一个参数执行一次和五次,结果应该一致,或者至少不会产生破坏性副作用。否则重试机制本身就变成风险。

5.3 日志与追踪

很多 Agent 看起来是“跑完了”,但你不知道它内部经历了什么。排查问题的前提是能看到每一步发生了什么。

不需要一开始就上重型的可观测系统。先做两件事:

  • 在关键节点记录结构化日志:节点名、输入摘要、输出摘要、耗时、错误信息。
  • 给每次调用打上 trace_id 或 request_id:让同一次请求的所有日志可以串起来查。

如果用了 LangChain 或 LangGraph 的官方回调机制,也能从中拿事件。但回调只是管道,真正有价值的是你在关键节点埋了什么日志。埋点时注意日志内容不要包含敏感信息,比如密钥、完整手机号、用户隐私内容。

5.4 资源占用怎么判断

学习阶段,一个普通笔记本就能跑。进入生产化之后,资源占用必须量化判断。

常见指标包括:

  • 单次请求的往返耗时。
  • 模型调用占用的显存或内存。
  • 并发上升时,CPU、内存、网络 IO 的变化。
  • 工具调用阻塞 Agent 的最大时间。

低配置环境不是不能试,但要把参数降下来:减少并发数、缩短上下文长度、限制工具数量、避免大文档一次性放入上下文。我的经验是,先用小样本把流程逻辑验证清楚,再按资源上限慢慢加压,不要一上来就开最大并发。

6. 三层排查链路

6.1 模型层:报错、空输出、格式乱

模型层的问题最容易误导人,因为报错可能出现在调用之后,也可能出现在调用之前。

排查顺序:

  1. 先看原始模型返回内容,不要只看最终结果。
  2. 确认 API Key、模型名、base_url 是否正确。
  3. 确认消息结构是否符合模型要求,比如 role 字段是否合法。
  4. 确认上下文长度有没有超过模型限制。
  5. 确认返回内容是否被工具解析器正确读取。

如果模型返回为空,先看是不是 Prompt 要求不明确,再看是不是上下文被截断,最后再考虑模型本身的问题。不要一上来就责怪“模型能力不行”。

6.2 工具层:参数绑定、权限、路径

工具层出问题时,错误信息通常很直白,但容易被忽略。

典型问题:

  • 工具名拼写不一致。
  • 模型生成的工具参数是字符串,但函数需要数字或对象。
  • 工具执行目录不对,相对路径找不到文件。
  • 工具运行环境缺少权限,读不了文件或连不上服务。
  • 工具返回内容过大,导致后续模型读取困难。

排查时先把工具单独拿出来测一遍,用固定参数调用,确认工具本身没问题,再把 Agent 接回来。这样能快速区分是模型理解的问题,还是工具实现的问题。

6.3 编排层:卡住、循环、状态丢失

编排层的问题往往是最隐蔽的,因为代码没报错,但流程就是不对。

常见现象和判断方法:

现象优先排查点
Agent 一直调用同一个工具条件边是否失效,最大迭代轮数是否配置
多轮之后忘记之前的内容State 字段是否被覆盖,记忆是否持久化
流程走到不期望的节点条件判断读取的字段是否被更新
状态恢复后结果不一致Checkpointer 和线程 ID 是否配对
偶发失败是否有超时、重试、日志是否完整

遇到编排层问题,我会先把整个图的节点和边打印出来,确认图结构不是从网上复制后随意改造出来的。然后看状态流转到哪个节点开始偏离预期,最后在偏离节点里加详细日志。

注意:不要同时修改多个参数再跑测试。一次只改一个变量,否则出了问题很难定位是哪次修改引入的。

7. 3小时学习路线和落地边界

7.1 学习路线怎么安排

按标题说的 3 小时,可以这样拆:

第一个小时:LangChain 核心组件。重点看模型封装和 Tool 定义,跑通一个“模型调用 + 工具调用”的最小示例。不要在这阶段研究所有文档,目标是知道每次调用输入输出长什么样。

第二个小时:LangGraph 状态图。把线性链改成图,理解 State、节点、条件边。跑通带条件分支的流程,多打印几次状态变化,感受图执行和普通函数调用的区别。

第三个小时:Harness 工程控制。给 Agent 加超时、重试、日志、输入输出校验。把一个只能“跑通”的 Demo,改造成一个能“看到每次调用发生了什么”的可控程序。

这三个小时结束后,你应该建立一条主线:组件从哪拿、流程怎么编排、工程控制怎么做。剩下的是查文档、看源码、踩业务坑,这些是持续积累的部分。

7.2 什么情况不需要这套框架

不是所有项目都需要 LangGraph 加 Harness。如果只是做一个单轮问答、固定提示词的接口调用,直接用普通 Chain 或者直接调用模型 SDK 反而更清晰。

以下情况可以考虑简化:

  • 流程是固定的线性步骤,没有分支和循环。
  • 工具数量很少,且调用顺序固定。
  • 请求量和风险都很低,不需要复杂重试和追踪。
  • 团队成员对 LangGraph 不熟悉,维护成本高于收益。

框架的最大价值在复杂流程里体现。简单任务强行上框架,只会增加样板代码和排查负担。

7.3 生产化之前必须补的工程项

如果确定要上生产,下面这几项建议放在功能开发之前考虑:

  • 输入输出校验:用户输入不可信,工具返回不可信,都要做类型和长度校验。
  • 敏感信息过滤:日志、错误信息里不要出现密钥、手机号、身份证等敏感字段。
  • 失败重试和任务队列:长时间任务或高频任务,不能只靠同步调一次。
  • 版本锁定:LangChain、LangGraph、模型封装库的版本要固定,不然升级一次就可能出现 API 不兼容。
  • 压测:用小并发、中并发、目标并发分别跑一轮,确认资源曲线和失败率。

最后留一个我自己的判断习惯:先把单任务跑稳,再考虑批量和接口。很多项目失败不是因为 Agent 不聪明,而是前置环境、输入格式和失败重试没有处理干净。DeepAgent 这套框架真正落地时,最该盯住的不是功能列表,而是状态传递是否干净、工具调用是否稳定、日志是否够用。按这个顺序做,至少能让你在 3 小时之后,不仅“听说过”这套技术栈,还能自己动手改出一个可靠版本。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 16:02:48

Alacritty 深度解析:一块 GPU 如何扛起终端的全部渲染

Alacritty 深度解析:一块 GPU 如何扛起终端的全部渲染 【免费下载链接】alacritty A cross-platform, OpenGL terminal emulator. 项目地址: https://gitcode.com/GitHub_Trending/al/alacritty Alacritty 是用 Rust 写的跨平台 OpenGL 终端模拟器&#xff0…

作者头像 李华
网站建设 2026/9/4 1:57:03

ESP32-S3 SPI配置实战:从白屏到点亮屏幕的完整指南

很多做 ESP32-S3 项目的朋友,第一次点亮 SPI 屏幕或者挂载 SPI 传感器时,都会遇到一个非常典型的现象:代码编译通过、下载正常、上电后屏幕却白屏,或者读取传感器数据全是 0xFF。排查半天,最后发现不是代码逻辑问题&am…

作者头像 李华
网站建设 2026/9/10 3:04:16

Linux命令行入门教程:从基础命令到Shell脚本与运维实践

Linux 命令行是运维工程师和程序员都绕不开的基本功。只要你会使用ls、cd、grep这类命令,你就能操作服务器、排查日志、写部署脚本,也能更好地理解 Docker、Kubernetes、CI/CD 这些现代基础设施背后发生了什么。很多初学者会被命令的数量吓到&#xff0c…

作者头像 李华
网站建设 2026/9/3 6:42:25

把重复的活儿交给 AI:Awesome Claude Skills 的 10 个实用技能模块

把重复的活儿交给 AI:Awesome Claude Skills 的 10 个实用技能模块 【免费下载链接】awesome-claude-skills A curated list of awesome Claude Skills, resources, and tools for customizing Claude AI workflows 项目地址: https://gitcode.com/GitHub_Trendin…

作者头像 李华
网站建设 2026/9/4 4:40:13

AI裹挟下的工程判断:从模型边界到落地实践的筛选方法

技术圈最近流行一个问题:我们是不是正在被 AI 裹挟着前进?英文标题是 "Are We Being Railroaded by AI?",字面意思是“我们是不是被 AI 强行推上了轨道”,但更深一层的意思是:在 AI 这股浪潮里,…

作者头像 李华