
1. 从一次 Agent 死循环说起LangChain 的固定链路为什么撑不住多步任务先说一个我踩过的坑。去年做一个代码审查 Agent需求很朴素读文件、找问题、给建议、等人工确认、再决定要不要继续。用 LangChain 的SequentialChain拼了七步跑起来看着挺顺直到测试同学丢进来一个 3000 行的文件——Agent 在第 3 步发现问题太多需要分批处理然后就没有然后了。因为链路是写死的它没法回到第 2 步重新切分只能硬着头皮往下走最后输出一份驴唇不对马嘴的报告。这就是传统 LangChain Chain 的结构性限制它是一条单向流水线。输入 → 步骤1 → 步骤2 → … → 输出每一步按顺序执行步数在编译期就定死了。你当然可以在 Chain 里塞if/else但那是 Python 层面的分支不是编排层面的分支——状态怎么在分支间传递、循环怎么收敛、中途怎么暂停全得你自己手写胶水代码。LangGraph 换了个思路。它把 Agent 建模成有向图节点Node是操作边Edge是流转规则所有节点共享一份 State。边可以是条件的图可以成环执行可以在某个节点挂起等外部输入。一句话概括Chain 是流水线LangGraph 是状态机。这篇面向的是正在做多步 Agent 选型的同学——你已经会写 LangChain现在纠结要不要迁到 LangGraph。我会给出两套最小可跑示例的依赖清单和配置片段并且用 TaoToken 的统一 Key 通道把两边都跑一遍对比状态管理和循环编排上的真实差异。TaoToken 在这里的作用是不管你底层调哪个模型Base URL 和 Key 都不用改省得在选型阶段还要折腾多套凭证。适合谁看写过至少一个 LangChain Chain、被固定步数卡过、想搞清楚 LangGraph 到底解决什么问题的开发者。不适合完全没碰过 LLM 应用的同学因为下面会直接上代码。2. TaoToken 统一 Key 前置一次配置两套框架共用选型阶段最烦的不是写代码是环境。LangChain 和 LangGraph 都要调模型如果每个框架配一套 Key、一套 Base URL对比实验还没开始就先累了。TaoToken 的价值就在这里它是一个兼容 OpenAI 接口规范的统一通道你拿一个 KeyLangChain 和 LangGraph 都能用同一份配置。先拿 Key。打开 https://taotoken.net/api-keys 登录后创建一个 API Key复制出来形如sk-xxxxxxxx。注意这个 Key 只在创建时完整显示一次丢了就重新建。然后确认两件事Base URL 用https://taotoken.net/api注意不要带任何查询参数Model ID 用你在控制台里看到的模型名比如gpt-4o-mini或claude-3-5-sonnet具体以 https://taotoken.net/models 页面为准依赖清单如下两个框架装在一起不冲突pip install langchain0.3.7 \ langchain-openai0.2.9 \ langgraph0.2.45 \ python-dotenv1.0.1环境变量统一放.envTAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELgpt-4o-mini这里有个细节值得说LangChain 的ChatOpenAI和 LangGraph 里用的模型客户端底层都是 OpenAI SDK所以只要base_url和api_key指向 TaoToken两边行为一致。这意味着你做选型对比时唯一的变量是编排框架本身而不是模型通道——这对得出可信结论很重要。如果你更习惯用配置文件而不是环境变量可以写一个config.toml[llm] api_key sk-你的key base_url https://taotoken.net/api model gpt-4o-mini temperature 0 [graph] max_iterations 8 checkpoint memory读取时用tomllibPython 3.11或tomli。我倾向 TOML因为 LangGraph 的图配置项会越来越多塞环境变量迟早乱。配置好之后先跑一个最小连通性测试确认 Key 和 Base URL 没问题from langchain_openai import ChatOpenAI import os from dotenv import load_dotenv load_dotenv() llm ChatOpenAI( modelos.getenv(TAOTOKEN_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), temperature0, ) print(llm.invoke(用一句话说明什么是状态机).content)能打印出内容说明通道通了。这一步别跳过后面所有对比都建立在这条通道上。3. 可复制配置LangChain Chain 与 LangGraph StateGraph 的最小对照现在上两套最小示例做同一件事给定一个任务判断是否需要继续处理需要就循环不需要就结束。这个场景足够小但恰好能暴露两者在状态管理和循环编排上的根本差异。3.1 LangChain 版本固定步数的线性 Chainfrom langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser import os from dotenv import load_dotenv load_dotenv() llm ChatOpenAI( modelos.getenv(TAOTOKEN_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), temperature0, ) analyze_prompt ChatPromptTemplate.from_template( 分析这个任务{task}。如果还需要继续处理回复 CONTINUE否则回复 DONE。 ) analyze_chain analyze_prompt | llm | StrOutputParser() def run_chain(task: str, max_steps: int 3): history [] for step in range(max_steps): decision analyze_chain.invoke({task: task}) history.append(decision.strip()) print(f[step {step}] decision{decision.strip()}) if DONE in decision: break return history if __name__ __main__: run_chain(把一份 3000 行代码拆成可审查的片段)注意这里的循环是用 Python 的for写的不是 Chain 本身的能力。max_steps是硬编码的你没法让 Chain 自己决定再跑一轮。而且history是外部变量每一步之间没有共享状态的概念——如果第 2 步需要第 1 步的中间结果你得手动传参。3.2 LangGraph 版本带条件边的 StateGraphfrom typing import TypedDict, Annotated import operator from langgraph.graph import StateGraph, END from langchain_openai import ChatOpenAI import os from dotenv import load_dotenv load_dotenv() llm ChatOpenAI( modelos.getenv(TAOTOKEN_MODEL), api_keyos.getenv(TAOTOKEN_API_KEY), base_urlos.getenv(TAOTOKEN_BASE_URL), temperature0, ) class TaskState(TypedDict): task: str decisions: Annotated[list, operator.add] iterations: int def analyze_node(state: TaskState): resp llm.invoke( f分析任务{state[task]}。需要继续回复 CONTINUE否则回复 DONE。 ) decision resp.content.strip() print(f[iter {state[iterations]}] decision{decision}) return { decisions: [decision], iterations: state[iterations] 1, } def should_continue(state: TaskState): if state[iterations] 8: return END if state[decisions] and DONE in state[decisions][-1]: return END return analyze workflow StateGraph(TaskState) workflow.add_node(analyze, analyze_node) workflow.set_entry_point(analyze) workflow.add_conditional_edges( analyze, should_continue, {analyze: analyze, END: END}, ) app workflow.compile() if __name__ __main__: result app.invoke({ task: 把一份 3000 行代码拆成可审查的片段, decisions: [], iterations: 0, }) print(最终决策序列, result[decisions])两段代码放一起看差异立刻出来了维度LangChain ChainLangGraph StateGraph循环控制Pythonfor硬编码条件边 图自环状态载体外部变量手动传共享TaskState终止条件代码里写死should_continue动态判断可暂停不支持配合 checkpointer 支持可视化无图结构可导出关键在Annotated[list, operator.add]这一行。它告诉 LangGraphdecisions字段每次更新是追加而不是覆盖。这是 LangGraph 状态管理的核心机制——reducer。没有它节点返回的新值会直接替换旧值历史就丢了。should_continue是条件边函数它只返回下一个节点的名字或END。图引擎根据返回值决定跳转而不是靠 Python 控制流。这就是编排层面的分支和代码层面的分支的区别。4. 验证请求用 TaoToken 跑通两套示例并对比结果配置写完了得实际跑一遍才算数。我用同一个任务分别跑两套代码观察输出。先跑 LangChain 版本python chain_demo.py输出类似[step 0] decisionCONTINUE [step 1] decisionCONTINUE [step 2] decisionCONTINUE到max_steps3就停了因为循环次数是写死的。任务其实没完成但 Chain 不知道它只会跑满三轮然后退出。这就是固定步数的悲哀——你要么把max_steps调大赌它能收敛要么写一堆判断逻辑但判断逻辑本身又得放在 Chain 外面。再跑 LangGraph 版本python graph_demo.py输出类似[iter 0] decisionCONTINUE [iter 1] decisionCONTINUE [iter 2] decisionDONE 最终决策序列 [CONTINUE, CONTINUE, DONE]图在第 3 轮自己判断出 DONE然后走END退出。iterations上限 8 只是保险丝正常情况下用不到。整个过程中decisions列表在 State 里累积每个节点都能看到完整历史——这在 Chain 里得靠手动维护一个 list 才能做到。如果你想验证状态是否真的共享可以在analyze_node里打印state[decisions]你会看到它每轮都在增长。这是 LangGraph 最实用的特性之一节点之间通过 State 通信而不是通过函数返回值层层传递。再补一个验证把should_continue里的iterations 8改成 2重跑你会看到图在第 2 轮强制结束。这说明终止条件完全由你控制且是编排层面的不需要改节点函数。用 TaoToken 通道跑这两套代码时我特意观察了延迟和稳定性。因为两边走的是同一个 Base URL 和 Key模型响应时间基本一致差异只来自框架本身的调度开销。实测下来LangGraph 因为多了图引擎的状态合并单轮开销比裸 Chain 略高一点点但在多步场景下它省掉的胶水代码和调试时间远超这点开销。5. 本篇常见错排查401、local proxy failed 与 reading choices 报错跑上面代码时最容易撞的几个错我按出现频率排一下。401 Unauthorized。九成是 Key 没读到。检查.env是否被load_dotenv()加载以及变量名是否拼错。TaoToken 的 Key 形如sk-开头如果你复制时带了空格或换行也会 401。可以加一行print(os.getenv(TAOTOKEN_API_KEY)[:8])确认前几位。local proxy failed / connection error。这类报错通常是 Base URL 写错了。正确值是https://taotoken.net/api不要写成https://taotoken.net/api/v1或带尾斜杠。OpenAI SDK 会自己拼/chat/completions你多写一层路径就 404 或连接失败。另外确认你的网络能正常访问该域名公司内网如果有出口限制需要找运维放行。reading choices of undefined。这个报错说明请求发出去了但返回体里没有choices字段。常见原因有两个一是 Model ID 写错服务端返回了错误对象而不是正常响应二是base_url指向了非 OpenAI 兼容的端点。解决办法是打印完整响应import httpx resp httpx.post( https://taotoken.net/api/chat/completions, headers{Authorization: fBearer {os.getenv(TAOTOKEN_API_KEY)}}, json{model: os.getenv(TAOTOKEN_MODEL), messages: [{role: user, content: hi}]}, timeout30, ) print(resp.status_code, resp.text[:500])看到原始返回问题基本就定位了。OAuth / token refresh 相关报错。如果你用的是某些需要 OAuth 的客户端比如 Claude Code 或 Codex 的 CLI报错可能来自凭证刷新失败。这类工具通常有自己的配置文件比如 Codex 的~/.codex/auth.json里面需要填 Base URL、Key 和 Model ID 三件套。以 Codex 为例auth.json大致长这样{ OPENAI_API_KEY: sk-你的key, OPENAI_BASE_URL: https://taotoken.net/api, OPENAI_MODEL: gpt-4o-mini }三个字段缺一不可。只填 Key 不填 Base URL它会去连默认端点自然失败。Claude Code 的配置类似在 settings 里指定ANTHROPIC_BASE_URL和ANTHROPIC_API_KEYModel ID 单独指定。如果你用 CC Switch 这类切换工具记得切换后重启终端环境变量不会热更新。LangGraph 特有的报错InvalidUpdateError或Expected dict, got ...。这通常是节点函数返回了非 dict或者返回的字段不在 State 定义里。LangGraph 对 State schema 校验比较严节点只能返回 State 里声明过的字段。检查你的TypedDict定义和return语句是否对得上。图跑飞了不收敛。如果你忘了在should_continue里加迭代上限图可能无限循环直到触发递归限制默认 25 次然后抛GraphRecursionError。养成习惯任何带自环的图都加一个iterations计数器兜底。6. 选型结论与下一步什么时候该上 LangGraph回到最初的问题为什么要用 LangGraph 而不是传统 LangChain我的判断标准很简单看你的 Agent 需不需要根据执行结果决定下一步。如果流程是固定的——输入进来过三个 Chain输出——那 LangChain 完全够用别为了新框架而新框架。但只要你遇到下面任意一条就该考虑 LangGraph需要循环且循环次数事先不知道需要条件分支分支依据是运行时结果需要在关键节点暂停等人工确认需要多个 Agent 协作共享一份状态需要中途持久化崩了能恢复反过来简单的问答机器人、固定步数的文本处理流水线、一次性任务用 Chain 更快上手没必要引入图的概念。如果你决定试 LangGraph下一步可以这样走先用 TaoToken 的统一 Key 把本文的两套示例跑通确认通道没问题然后把 LangChain 版本里那个手动维护 history的逻辑改写成 LangGraph 的 State reducer感受一下状态管理的差异最后加一个MemorySavercheckpointer试试在节点间暂停和恢复。TaoToken 的接入文档在 https://taotoken.net/doc 里面有各框架的配置示例。模型列表在 https://taotoken.net/models 选型阶段可以多试几个模型对比效果。如果你要长期跑编码类 Agent可以看看 Coding Plan按量计费比单次调用划算。想先感受一下模型对话效果直接开 https://taotoken.net/chat 就能试。工具是为目标服务的。LangGraph 不是 LangChain 的替代品而是它在复杂编排场景下的补充。搞清楚边界比盲目追新重要得多。