放弃LangChain:我手写了一个270行ReAct循环

作者用两天亲历LangChain的痛,删掉框架手写270行ReAct循环,论证可审计性优先。
作者花两天尝试让 LangChain 的 AgentExecutor 按预期工作,虽然在功能上跑通了,却因不透明性、提供商锁定和 API 频繁变动三重困境而陷入调试泥潭:看不到决策原因、切换 Ollama 时追踪系统崩溃、框架接口半年一变。于是他删除 LangChain,手写约 270 行的 ReAct 循环,核心逻辑仅约 20 行,涵盖有界迭代、工具调度、结构化消息与最终答案回退。他通过 LLMProvider 适配器模式实现提供商无关,核心层永不导入厂商 SDK;并在添加第二个工具之前先构建结构化追踪系统,把每次 LLM 调用、工具调用与错误记录为数据模型。踩坑经验包括:横切关注点应放核心层而非适配器层;先用一个工具验证架构再扩展。结论是框架并非错,只是不一定总对——当系统价值在于可审计性、控制力与安全时,自己写循环更值得。
两天折腾LangChain之后,我删掉了它
作者花了两天时间试图让 LangChain 的 AgentExecutor 按预期工作。结果呢?它确实能跑——Agent 会调用工具,也能产出答案。但问题在于:他无法解释 Agent 为什么做出那些选择。切换 LLM 提供商时,追踪系统直接崩溃;添加第二个工具时,调度逻辑纠缠在他没写过、也看不懂的抽象层里。
于是他做了一个决定:删掉 LangChain,手写一个约 270 行的 ReAct 循环。

这不是一篇"框架无用论"的文章。作者坦承 LangChain、CrewAI 这类框架提供了真实价值——预构建的工具集成、记忆抽象、回调系统、多 Agent 编排。对于需要快速集成大量工具、或者框架设计理念恰好匹配你需求的项目,它们确实是合理的加速器。但对于 RepoPilot 这个项目,三件事让框架成了负担。
框架失灵的三重困境
第一,不透明性。 当 AgentExecutor 做出决策时,作者无法轻松看到背后的原因。提示链、工具选择逻辑、中间解析过程——全都藏在他没写过的代码层里。调试意味着阅读框架源码,而不是自己的代码。
第二,提供商锁定。 LangChain 虽然抽象了 LLM 提供商,但这种抽象有自己特定的形状。当作者尝试从 Gemini 切换到本地 Ollama 模型时,追踪系统直接崩溃——框架的回调系统与 LLM 抽象层绑定得太深,超出了他的控制范围。
第三,API 频繁变动。 LangChain 的接口变化速度很快,半年前写的代码往往需要重写。对于一个计划长期维护和扩展的项目来说,这是实实在在的负债。
单独看每一条,都不足以判框架死刑。但合在一起,指向了一个清晰的答案:当一个项目的全部价值在于可审计性和控制力时,黑盒就是错误的地基。
LangChain 的 AgentExecutor 是什么? 它是 LangChain 为构建 Agent 提供的高层运行时封装。开发者传入一个 LLM、一组工具和一个提示模板,AgentExecutor 会自动接管整个循环:调用模型、解析模型输出的工具调用意图、执行工具、把结果拼回对话历史,直到模型给出最终答案。它还内置了最大迭代次数、异常处理、回调钩子等机制。问题正源于这种“全包办”:循环里的每一步都经过多层内部抽象(提示组装、输出解析器、回调管理器),这些代码由框架维护而非使用者编写。当行为不符合预期时,你无法直接在业务代码里下断点,而必须深入框架源码去理解提示被如何改写、工具调用被如何解析——这与作者强调的“可审计性”诉求直接冲突。
手写ReAct循环:20行核心逻辑
ReAct(Reason + Act,推理+行动)本质是一个极简模式:LLM 推理下一步该做什么 → 选择工具并提供参数 → 工具执行并返回结果 → 结果加入对话 → 循环直到 LLM 产出最终文本回复。
def run(self, goal: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": goal},
]
for step in range(self.max_steps):
response = self.llm.chat_with_tools(messages, self.tools.schemas())
if not response.tool_calls:
return response.content
messages.append({"role": "assistant", "content": response.content})
for call in response.tool_calls:
result = self.tools.execute(call.name, call.arguments)
messages.append({
"role": "tool",
"name": call.name,
"content": json.dumps(result),
})
return self._request_final_answer(messages)
大约 20 行核心逻辑,覆盖了全部需求:通过 max_steps 实现有界迭代、通过注册表完成工具调度、可随时检查的结构化消息、以及达到步数上限时的最终答案回退机制。
作者写道:"是我写的,我理解每一行。出问题时,我清楚地知道该看哪里。"
提供商抽象的代价与收益
一个早期就做对的设计决策是:核心层永远不导入任何提供商的 SDK。Agent 与接口对话,而不是直接与 Gemini、OpenAI 或 Ollama 对话。
class LLMProvider(ABC):
@abstractmethod
def chat(self, messages: list[dict]) -> str: ...
@abstractmethod
def chat_with_tools(self, messages: list[dict], tools: list[dict]) -> NormalizedResponse: ...
Gemini 适配器实现这个接口,切换提供商只需写一个新适配器,而非重写 Agent。开发者可以用免费的云端 API 开发,后期通过改一个环境变量切到本地模型,Agent 完全无感知。
这个适配器模式多花了约 50 行代码,换来的是 Phase 2 必需的灵活性。作者特别强调:一旦在核心层写下 from openai import OpenAI,就等于把自己锁死了。
适配器模式与依赖倒置。 适配器模式(Adapter Pattern)是经典设计模式之一:定义一个稳定的抽象接口,为每个具体实现编写一个“适配器”把第三方 API 转换成该接口。作者的做法本质上是依赖倒置原则(Dependency Inversion Principle)的落地——高层模块(Agent 核心循环)不依赖低层模块(各厂商 SDK),二者都依赖抽象接口 LLMProvider。这样做的直接收益是:核心逻辑对“谁在提供推理能力”完全无感知,切换成本被压缩到只写一个新的适配器类。代价则是需要把不同厂商的响应格式归一化成统一的 NormalizedResponse,包括工具调用字段的差异(如 OpenAI 的 tool_calls 与 Gemini 的 functionCall)。这层归一化代码虽然增加了约 50 行,但它把变化隔离在边界处,避免污染核心循环。
可观测性:大多数教程跳过的关键环节
在添加第二个工具之前,作者先构建了追踪系统。每次 Agent 运行都产出结构化追踪数据——每一次 LLM 调用、工具调用、结果、错误,都以数据模型而非日志行的形式记录。
@dataclass
class AgentStep:
step_number: int
timestamp: float
type: str # "llm_call" | "tool_call" | "tool_result" | "error"
model: str | None
tool_name: str | None
arguments: dict | None
result: Any | None
error: str | None
duration_ms: float
每次运行结束后,RepoPilot 打印一份摘要(目标、总步数、LLM 调用次数、工具调用次数、错误数、总耗时),并将完整追踪写入 .runs/{run_id}.json。
这才是让 Agent 可调试的关键。运行失败时不需要猜测——打开追踪文件,就能看到确切发生了什么:哪个提示词被送入、哪个工具被调用、返回了什么、循环在哪里断裂。
作者为什么在添加更多工具之前先做追踪?他的理由很实在:如果有 5 个工具且循环有问题,你根本不知道是哪个环节出了错。有了追踪,你能看到循环本身运行完美而某个工具行为异常——反之亦然。
什么是可观测性,为什么对 Agent 尤其重要? 可观测性(Observability)源自控制论,指仅通过系统对外输出的数据就能推断其内部状态的能力,在软件工程中通常由日志、指标、追踪三大支柱构成。对传统程序而言,调试器可以暂停执行、检查变量;但 LLM Agent 的决策路径由概率性模型生成,本质上是不可复现的黑盒——同样的输入可能产生不同的工具调用序列。因此,事后追踪成为唯一有效的诊断手段。作者用 @dataclass 定义结构化 AgentStep,而非打印字符串日志,其关键区别在于:结构化数据可被程序化查询、聚合和回放,比如统计“第 3 步工具调用了 1200ms 是否超时”、对比两次运行的调用序列差异。这正是让 Agent 从“只能祈祷”变成“可以调试”的分水岭。
两个值得分享的踩坑经验
第一,一开始把追踪逻辑放在了提供商适配器里。 第一版将 LLM 调用记录写在 Gemini 适配器内部,看起来合理——API 调用确实发生在那里。但切换提供商时,追踪就会丢失。后来他把追踪移到了核心循环,现在无论哪个提供商运行,追踪行为完全一致。教训是:横切关注点(追踪、安全、审批)应放在核心层,而非提供商层。
第二,一上来就写了 5 个工具。 初稿同时写了 git_status、git_diff、read_file、list_files 和 search_code。结果花了一整天调试工具 schema 和错误处理——而此时他甚至还不确定 ReAct 循环本身是否正确。最终砍到只剩一个工具 git_status,刚好够证明循环能工作。教训是:一个工具就足以验证架构,功能扩展要等基础设施稳固之后。
横切关注点(Cross-Cutting Concern)。 这是软件架构中的术语,指那些会散落在多个模块、无法自然归属单一职责的功能,例如日志、追踪、鉴权、事务、缓存。它们的特点是:如果按照“谁调用谁负责”的直觉去放置,就会在各个具体实现里重复出现,并且一旦新增实现就容易被遗漏——正如作者把追踪写进 Gemini 适配器后,切换提供商时追踪就消失了一样。经典解法是面向切面编程(AOP)思想或把它上提到核心层统一处理。作者的教训正好印证了这一点:追踪逻辑放在核心循环中,无论背后对接哪个提供商,行为都一致;反之把横切关注点下沉到适配器层,就等于把一致性寄托在“每个适配器作者都记得实现它”之上,长期必然出错。
Phase 1完成:一个可理解的基础
当前状态:Agent 通过提供商无关接口与 Gemini 对话,调用只读的 git_status 工具,记录每次运行的结构化 JSON 追踪,通过 68 个自动化测试,在真实本地仓库上端到端运行。
这还不是一个 CI 修复 Agent,但它是实现该目标的基础。而这个基础是干净的、经过测试的、最重要的是——可理解的。
据作者介绍,Phase 2 才是 RepoPilot 名副其实的阶段:用确定性方式解析 GitHub Actions 日志(对已知故障模式不使用 LLM),仅在模式模糊时才升级到 LLM,以 diff 形式提出修复但不经人工审批绝不应用,追踪每一个决策。他认为确定性优先的设计才是差异化所在——大多数 CI 修复 Agent 直接把日志扔给 LLM 碰运气,而 RepoPilot 会先用正则分类故障,只在必要时才调用 LLM。
核心结论
框架不是错的,只是不一定总对。快速原型用 LangChain 没问题;但如果你构建的系统,其价值在于可审计性、控制力和安全保障——自己写循环。大约 100 行代码,你会理解每一行。
而且,先构建可观测性。不是因为有趣,而是因为它决定了一个 Agent 是你能够调试的,还是只能祈祷它能正常工作的。
相关推荐

ClaudeMem:让Claude、ChatGPT、Grok共享记忆的AI Agent记忆层
ClaudeMem为Claude、ChatGPT、GrokBot等多个AI Agent提供共享记忆,解决跨模型协作时的上下文断裂问题。本文解析其本地优先架构、安装接入方式、实际工作流用法,以及创作者对未来单一编排者Bot的设想。

OpenAI Codex CLI实战指南:终端里的开源AI编程代理
OpenAI Codex CLI是一款运行在终端里的开源AI编程代理。本文梳理其2025年发布、2026年桌面版及与ChatGPT合并的演进脉络,并用三个步骤说明安装配置、发起任务与审查迭代的实操路径。

AI入门课程实测:ChatGPT、Gemini与AI智能体实战
一套面向初学者的完整AI课程实测解读:覆盖ChatGPT、Gemini、Notebook LM、Manus与DeepSeq,包含18000条数据集分析、AI智能体搭建网站等实操,讲解大语言模型原理与局限,适合营销、金融、数据、产品与软件岗位。