这两天把 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 模型层:报错、空输出、格式乱
模型层的问题最容易误导人,因为报错可能出现在调用之后,也可能出现在调用之前。
排查顺序:
- 先看原始模型返回内容,不要只看最终结果。
- 确认 API Key、模型名、base_url 是否正确。
- 确认消息结构是否符合模型要求,比如 role 字段是否合法。
- 确认上下文长度有没有超过模型限制。
- 确认返回内容是否被工具解析器正确读取。
如果模型返回为空,先看是不是 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 小时之后,不仅“听说过”这套技术栈,还能自己动手改出一个可靠版本。