LangChain 还是 pi-agent?两套 Agent 框架的哲学差异(含同任务实测对照)
「我该用 LangChain 还是自己写 / 换别的?」——这是开始做 Agent 时最常见的纠结。这篇把 LangChain(JS) 和 pi-agent 摆在一起比:不是比谁的 API 漂亮,而是比它们对「Agent 应该由谁来管」这件事的答案。
为了不空谈,我给同一类任务(一个会调工具的查天气 Agent)在两边各写了一份代码并真跑起来:pi 侧用内置的 fauxProvider(不需要 key),LangChain 侧用真实模型端点(DeepSeek 的 Anthropic 兼容接口)。文中的事件序列、消息序列、最终回答都是真实输出。
版本:
@earendil-works/pi-ai/pi-agent-core0.85.1;langchain1.5.11 /@langchain/langgraph1.4.15(2026-09 npm 实测)。本站已有两个系列:pi-agent 系列、LangChain + LangGraph 系列。
一、先给结论
| LangChain / LangGraph | pi(pi-ai + pi-agent-core) | |
|---|---|---|
| 一句话 | 给你一整套「Agent 平台」:抽象、集成、可观测、生态 | 给你一层薄而显式的「harness」:循环、事件、会话,别的你自己写 |
| 气质 | 平台化、约定多、上手快 | 极简、透明、可控 |
| 适合 | 快速搭原型 / 接大量外部集成 / 团队协作与可观测 | 想完全掌控循环 / 深度定制 / 嵌入自己的产品(如 InkOS) |
一句话选型:要生态和速度 → LangChain;要透明和掌控 → pi。
二、架构对照:抽象层数不一样
两边看起来都是「两层」,但厚薄完全不同:
- LangChain 的
core + LangGraph里塞的是方法论级抽象(Runnable、Chain、StateGraph、Reducer、Checkpointer、回调体系…); - pi 的两层里,
pi-ai只做「统一各家 LLM 的接入」,pi-agent-core只做「状态 + 工具循环 + 事件流」。它没有图、没有 reducer、没有 checkpointer 这些概念——因为它假设「循环之外的事你自己管」。
三、同任务对照:一个「查天气」Agent
pi 的写法(真实代码,35 行左右)
import { Type, createModels, fauxProvider, fauxAssistantMessage, fauxToolCall, fauxText } from '@earendil-works/pi-ai';
import { Agent } from '@earendil-works/pi-agent-core';
const faux = fauxProvider(); // ← 无需 key 的假模型
const models = createModels();
models.setProvider(faux.provider);
const agent = new Agent({
initialState: {
systemPrompt: '你是一个会调用工具的助手。',
model: faux.getModel(),
tools: [{
name: 'get_weather', label: '查天气', description: '查询某城市天气',
parameters: Type.Object({ city: Type.String() }), // ← TypeBox schema
execute: async (_id, { city }) => ({ content: [{ type: 'text', text: `${city}: 25°C 晴` }] }),
}],
},
streamFn: models.streamSimple.bind(models), // ← 只注入「怎么连模型」
});
agent.subscribe((e) => { /* 事件流:turn_start / message_end / tool_execution_* … */ });
await agent.prompt('杭州天气怎么样?');
真实运行输出(事件序列):
== turn_start ==
[message_end] user = [text]
[message_end] assistant = [toolCall] ← 模型提议调工具
[tool_execution_start] get_weather
[tool_execution_end] ok=true
[message_end] toolResult = [text]
== turn_end == toolResults=1
== turn_start == ← 自动进入下一轮
[message_end] assistant = [text] ← 读到工具结果后回答
== turn_end == toolResults=0
== agent_end == messages=4
LangChain 的写法(一行起 Agent)
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
const getWeather = tool(async ({ city }) => `${city}: 25°C 晴`, {
name: 'get_weather',
description: '查询某城市天气',
schema: z.object({ city: z.string() }), // ← zod schema
});
const agent = createReactAgent({ llm: model, tools: [getWeather] });
const res = await agent.invoke({ messages: [{ role: 'user', content: '查一下杭州的天气' }] });
真实运行输出(真实模型,两次工具调用):
▶ 工具被调用: get_weather(杭州) -> 25°C 晴
消息角色序列: HumanMessage -> AIMessage -> ToolMessage -> ToolMessage -> AIMessage
最终回答: "杭州:25°C 晴。"
(同一份 demo 里换成「查杭州和深圳人口」,模型会并行调两次工具,于是出现连续两个 ToolMessage。)
对照着看,差在哪
| pi | LangChain | |
|---|---|---|
| 模型接入 | models.streamSimple(注入一个函数) | 构造 Chat 模型实例(ChatAnthropic / ChatOpenAI) |
| 工具 schema | TypeBox(可 JSON 序列化) | zod(TS 优先) |
| 循环 | Agent 内部跑,事件全暴露 | createReactAgent 内部跑,消息全保留 |
| 你观察到的 | 事件序列(turn/message/tool_execution) | 消息序列(Human/AI/Tool) |
| 无 key 开发 | 内置 fauxProvider(一等公民) | 一般要真模型或自己搭 fake |
四、五条本质差异
1. 循环的「可见度」不同
pi 把循环暴露成事件流:turn_start / message_start / message_update / message_end / tool_execution_start|update|end / turn_end / agent_end。你订阅事件就能画进度条、做审计、接 UI——循环本身是可见的。
LangChain 把循环收进「图」里:你能拿到状态快照、能流式拿更新(streamMode: 'updates' | 'values'),但中间过程是消息和状态,不是「事件」。
2. 状态的「归属」不同
- pi:状态就是消息序列,加一层
AgentMessage → convertToLlm()的转换管线(可以把「只有 UI 看得懂的消息」混在里面,发给模型前过滤掉);记忆靠会话(sessionId,持久化有独立的 SQLite 后端包)。 - LangChain:状态是你显式声明的图状态——
Annotation.Root({...})+ reducer 决定「同一字段多次写入怎么合并」;记忆靠 checkpointer(按thread_id存)。更工程化,也更啰嗦(要理解 reducer 才能不出错)。
3. 工具与 schema 的取舍
- pi 用 TypeBox:schema 本身就是 JSON Schema,可序列化、可跨进程传(这也是它在 MCP/分布式场景顺手的原因);
- LangChain 生态用 zod:TS 推导最强、生态最大,但跨语言/进协议要转换(MCP 那篇 讲过这个链路)。
4. 「模型调用的自由度」不同
LangChain 想帮你把模型调用的花样都覆盖:withStructuredOutput(结构化输出)、bindTools、流式、回调、缓存……我在实测里也踩到过它的边界:推理模型不支持强制 tool_choice,withStructuredOutput 直接 400(错误原文 Thinking mode does not support this tool_choice)。
pi 的选择是只给原语:stream/streamSimple、complete/completeSimple、validateToolCall——能做多少取决于你写多少。
5. 体量与依赖
- pi 的
pi-agent-core依赖只有几个:pi-ai、pi-telemetry、chord、typebox、diff、ignore、yaml(本机从 package.json 读的); - LangChain 的依赖树大得多(厂商 SDK、SDK 适配层、core、langgraph……),能力多,装的东西也多。
五、各自的「隐藏优势」
LangChain 的优势在生态:文档加载器、向量库、检索器、LangSmith 可观测、海量集成——要快速拼一个 RAG/多工具应用,它省下的时间很实在。(前提是你能接受它的抽象;遇到边界时也要有能力往下钻。)
pi 的优势在透明:整个循环你能读懂、能改。这也是为什么 InkOS 这种生产 harness 会建在它上面——需要自己掌控「确认闸门、原子落盘、状态机」时,薄框架 + 自己写比「厚框架 + 绕开它」要顺。
还有一点:pi 的
Agent.subscribe事件流 +streamProxy(给浏览器走后端代理)+ 低层agentLoop,让它既能当「开箱 Agent」,也能拆开只用循环原语——分层的粒度是按「你要接管多少」设计的。
六、选型清单
| 你的情况 | 建议 |
|---|---|
| 快速验证想法、要接很多现成集成 | LangChain / LangGraph |
| 要图结构、条件分支、人工介入、持久化状态 | LangGraph(它的强项) |
| 要自己掌控循环/权限/落盘、做生产 harness | pi-agent-core |
| 只要「多厂商统一接入 + 工具调用原语」 | pi-ai 单用也成立 |
| 完全不想用框架 | 参考本站 frontend-agent,手写 while + 工具循环 |
混合用法也存在:用 LangGraph 管编排、用 pi-ai 当 LLM 接入层(或反过来只在某些节点用 LangChain 的集成)。但别为混而混——两套抽象叠在一起,调试成本会翻倍,除非确有一个「另一侧做不到」的刚需。
七、一句话收尾
LangChain 在回答「Agent 应该长什么样」,pi 在回答「Agent 循环最少要多小」。 前者给你一栋装好的房子,后者给你砖和图纸——选哪个,取决于你是想快点住进去,还是想自己决定户型。
关联
- pi-agent 系列:pi-ai 的统一模型层、pi-agent-core 的事件流与工具生命周期
- LangChain + LangGraph 系列:LCEL、状态图与 reducer、ReAct Agent 实战
- InkOS 架构拆解:一个真实建在 pi 之上的生产 harness
- 手写 agent 循环:不依赖任何框架的最小实现
参考
- pi 仓库/包:
github.com/earendil-works/pi(npm@earendil-works/pi-ai、@earendil-works/pi-agent-core) - LangChain JS 文档:js.langchain.com | LangGraph JS:langchain-ai.github.io/langgraphjs
- 本文实测:pi 侧
fauxProvider事件序列、LangChain 侧真实端点消息序列与工具调用日志,均为本机运行结果;版本如文首标注
