目录
- [引言:为什么需要 LangChain.js](#引言:为什么需要 LangChain.js "#1-%E5%BC%95%E8%A8%80%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-langchainjs")
- 核心架构设计哲学
- [实战案例:构建天气查询 Agent](#实战案例:构建天气查询 Agent "#3-%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8B%E6%9E%84%E5%BB%BA%E5%A4%A9%E6%B0%94%E6%9F%A5%E8%AF%A2-agent")
- 调用栈级别时序图
- 设计模式深度解析
- 关键实现细节
- 总结
1. 引言:为什么需要 LangChain.js
1.1 LLM 生态的碎片化问题
在 LangChain.js 出现之前,开发者面临三大挑战:
- API 碎片化:每个 LLM 提供商(OpenAI、Anthropic、Google)都有独特的 API 设计
- 组合困难:Prompt、模型、工具、输出解析器之间缺乏统一的组合方式
- 可观测性缺失:复杂的调用链难以追踪和调试
1.2 核心设计哲学:"Everything is a Runnable"
LangChain.js 的核心洞察是:所有 LLM 操作都可以抽象为"输入→输出"的转换过程。
ini
Prompt + LLM + Tools + Output Parser = 一个可组合的 Runnable
这种统一抽象带来三大优势:
- 可组合性(Composability):任意 Runnable 可以像管道一样连接
- 互操作性(Interoperability):不同提供商的组件可以无缝替换
- 可观测性(Observability):统一的回调系统追踪整个调用链
1.3 设计原则
| 原则 | 说明 | 实现方式 |
|---|---|---|
| 单一职责 | 每个模块只做一件事 | Runnable、Tool、Message 分离 |
| 开闭原则 | 对扩展开放,对修改封闭 | 中间件系统、回调系统 |
| 依赖倒置 | 依赖抽象而非具体实现 | RunnableInterface、StructuredToolInterface |
| 流式优先 | 原生支持流式处理 | AsyncGenerator、IterableReadableStream |
2. 核心架构设计哲学
2.1 Runnable 模式:Command + Strategy + Template Method
核心文件 :libs/langchain-core/src/runnables/base.ts
为什么要用这个模式?
在传统开发中,模型调用、链式执行、工具调用各有不同的 API:
typescript
// ❌ 碎片化的 API
await model.call(messages);
await chain.run(input);
await tool.execute(args);
LangChain.js 通过 Runnable 基类统一了所有操作:
typescript
// ✅ 统一的接口
await runnable.invoke(input);
await runnable.batch(inputs);
await runnable.stream(input);
三种设计模式的融合
1. Command Pattern(命令模式)
invoke()、batch()、stream() 是三种标准"命令",所有 Runnable 子类必须实现:
typescript
// libs/langchain-core/src/runnables/base.ts:124-148
export abstract class Runnable<
RunInput = any,
RunOutput = any,
CallOptions extends RunnableConfig = RunnableConfig,
> extends Serializable implements RunnableInterface<RunInput, RunOutput, CallOptions> {
// 抽象方法:子类必须实现
abstract invoke(
input: RunInput,
options?: Partial<CallOptions>
): Promise<RunOutput>;
// 默认实现:调用 N 次 invoke
async batch(
inputs: RunInput[],
options?: Partial<CallOptions> | Partial<CallOptions>[],
batchOptions?: RunnableBatchOptions
): Promise<(RunOutput | Error)[]> {
// ... 批量处理逻辑
}
// 默认实现:委托给 _streamIterator
async *_streamIterator(
input: RunInput,
options?: Partial<CallOptions>
): AsyncGenerator<RunOutput> {
yield this.invoke(input, options);
}
}
2. Strategy Pattern(策略模式)
每个组件提供自己的执行策略:
typescript
// ChatModel 的策略:调用 LLM API
class ChatOpenAI extends BaseChatModel {
async invoke(input: BaseLanguageModelInput, options?: Partial<CallOptions>) {
const promptValue = BaseChatModel._convertInputToPromptValue(input);
const result = await this.generatePrompt([promptValue], options, options?.callbacks);
return result.generations[0][0].message;
}
}
// Tool 的策略:执行具体功能
class DynamicStructuredTool extends StructuredTool {
protected async _call(arg: SchemaOutputT, runManager?: CallbackManagerForToolRun) {
return this.func(arg, runManager, config);
}
}
3. Template Method(模板方法模式)
_callWithConfig() 定义了标准生命周期:
typescript
// libs/langchain-core/src/runnables/base.ts:359-392
protected async _callWithConfig<T extends RunInput>(
func: (input: T, config?: Partial<CallOptions>, runManager?: CallbackManagerForChainRun) => Promise<RunOutput>,
input: T,
options?: Partial<CallOptions> & { runType?: string }
) {
const config = ensureConfig(options);
// 1. 启动回调
const callbackManager_ = await getCallbackManagerForConfig(config);
const runManager = await callbackManager_?.handleChainStart(
this.toJSON(),
_coerceToDict(input, "input"),
config.runId
);
let output;
try {
// 2. 执行子类逻辑
const promise = func.call(this, input, config, runManager);
output = await raceWithSignal(promise, config.signal);
} catch (e) {
// 3. 错误回调
await runManager?.handleChainError(e);
throw e;
}
// 4. 结束回调
await runManager?.handleChainEnd(_coerceToDict(output, "output"));
return output;
}
使用示例
typescript
// 组合多个 Runnable
const chain = promptTemplate.pipe(model).pipe(outputParser);
// 添加重试逻辑
const chainWithRetry = chain.withRetry({ stopAfterAttempt: 3 });
// 添加降级方案
const chainWithFallback = chain.withFallbacks([fallbackChain]);
// 批量处理
const results = await chain.batch([input1, input2, input3]);
2.2 中间件管道:Chain of Responsibility + Decorator
核心文件 :libs/langchain/src/agents/nodes/AgentNode.ts
为什么需要中间件?
Agent 需要在不修改核心逻辑的情况下扩展行为:
- 请求前:注入上下文、验证权限
- 模型调用:缓存、重试、认证
- 响应后:结果转换、日志记录
中间件钩子体系
typescript
interface AgentMiddleware {
name: string;
// Agent 执行前(一次性)
beforeAgent?: (state: AgentState, runtime: Runtime) => Promise<Command | void>;
// 模型调用前(每次循环)
beforeModel?: (state: AgentState, runtime: Runtime) => Promise<Command | void>;
// 包装模型调用(可替换整个调用逻辑)
wrapModelCall?: (
request: ModelRequest,
handler: WrapModelCallHandler
) => Promise<AIMessage | Command>;
// 模型调用后(每次循环)
afterModel?: (state: AgentState, runtime: Runtime) => Promise<Command | void>;
// Agent 执行后(一次性)
afterAgent?: (state: AgentState, runtime: Runtime) => Promise<Command | void>;
// 包装工具调用
wrapToolCall?: (
toolCall: ToolCall,
handler: WrapToolCallHandler
) => Promise<ToolMessage>;
}
装饰器链的构建原理
关键代码展示了如何从内向外包装:
typescript
// libs/langchain/src/agents/nodes/AgentNode.ts:452-463
let wrappedHandler = baseHandler; // 最内层:实际模型调用
// 从后向前遍历,使第一个中间件成为最外层包装
for (let i = wrapperMiddleware.length - 1; i >= 0; i--) {
const middleware = wrapperMiddleware[i];
if (middleware.wrapModelCall) {
const innerHandler = wrappedHandler;
const currentMiddleware = middleware;
// 创建新的包装层
wrappedHandler = async (request) => {
return currentMiddleware.wrapModelCall(request, innerHandler);
};
}
}
// 最终调用时,请求穿过所有中间件层
const response = await wrappedHandler(initialRequest);
执行流程(假设有 [Auth, Retry, Cache] 三个中间件):
请求进入
↓
Auth.wrapModelCall(最外层)
↓ 验证通过
Retry.wrapModelCall
↓ 失败时重试
Cache.wrapModelCall
↓ 检查缓存
baseHandler(实际模型调用)
↓ 返回结果
Cache(返回或存储缓存)
↓
Retry(捕获异常并重试)
↓
Auth(添加审计日志)
↓
返回最终结果
2.3 回调与追踪系统:Observer Pattern
核心文件 :libs/langchain-core/src/callbacks/manager.ts
为什么用观察者模式?
问题:在任意深度的组件组合中,如何追踪执行过程?
markdown
Agent
└─ Chain
└─ Prompt + LLM
└─ Tool
└─ API Call
传统方案需要在每层手动传递日志对象,LangChain.js 使用分层回调管理器:
typescript
// libs/langchain-core/src/callbacks/manager.ts:90-100
export abstract class BaseCallbackManager {
abstract addHandler(handler: BaseCallbackHandler): void;
abstract removeHandler(handler: BaseCallbackHandler): void;
abstract setHandlers(handlers: BaseCallbackHandler[]): void;
}
// 每次调用生成独立的 RunManager
export class BaseRunManager {
constructor(
public readonly runId: string,
public readonly handlers: BaseCallbackHandler[],
protected readonly inheritableHandlers: BaseCallbackHandler[],
// ...
) {}
// 创建子管理器(继承父级 handlers)
getChild(tag?: string): CallbackManager {
const manager = new CallbackManager(this.runId);
manager.setHandlers(this.inheritableHandlers); // 继承可传递的 handlers
return manager;
}
}
回调生命周期
typescript
// 1. 链式调用开始
await callbackManager?.handleChainStart(chain.toJSON(), input, runId);
try {
// 2. LLM 调用开始
const llmRunManager = await callbackManager?.handleLLMStart(
llm.toJSON(),
messages,
runId
);
// 3. 流式输出
for await (const chunk of llm._streamResponseChunks(messages, options, llmRunManager)) {
await llmRunManager?.handleLLMNewToken(chunk.text);
}
// 4. LLM 调用结束
await llmRunManager?.handleLLMEnd(result);
} catch (error) {
// 错误处理
await llmRunManager?.handleLLMError(error);
throw error;
}
// 5. 链式调用结束
await callbackManager?.handleChainEnd(output);
使用示例:自定义回调处理器
typescript
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
class MyTracingHandler extends BaseCallbackHandler {
name = "my_tracer";
async handleLLMStart(llm: Serialized, prompts: string[], runId: string) {
console.log(`[LLM Start] Run: ${runId}, Prompts: ${prompts.length}`);
}
async handleLLMNewToken(token: string, runId: string) {
process.stdout.write(token); // 实时显示
}
async handleLLMEnd(output: LLMResult, runId: string) {
console.log(`\n[LLM End] Run: ${runId}`);
}
async handleToolStart(tool: Serialized, input: string, runId: string) {
console.log(`[Tool Start] ${tool.name}, Input: ${input}`);
}
}
// 在调用时传入
const result = await agent.invoke(input, {
callbacks: [new MyTracingHandler()]
});
2.4 Agent 状态机:State Machine + Composite
核心文件 :libs/langchain/src/agents/ReactAgent.ts
为什么用状态机?
Agent 的核心是 ReAct 循环(Reasoning + Acting):
用户输入 → LLM 推理 → 是否需要工具?
├─ 是 → 执行工具 → 结果加入上下文 → 回到 LLM 推理
└─ 否 → 返回最终答案
硬编码循环无法处理:
- 条件路由(有时跳过工具)
- 中间件干预(提前终止、重试)
- 并行工具调用
LangGraph StateGraph 架构
typescript
// libs/langchain/src/agents/ReactAgent.ts:247-251
const workflow = new StateGraph(state, {
input,
output,
context: this.options.contextSchema,
});
核心节点:
scss
START
↓
[BeforeAgent Middleware Nodes] (一次性)
↓
[BeforeModel Middleware Nodes] (每次循环)
↓
model_request (AgentNode)
↓
有工具调用?
├─ 是 → tools (ToolNode) → 回到 BeforeModel 或 model_request
└─ 否 → [AfterModel Middleware Nodes] → [AfterAgent Middleware Nodes] → END
条件路由实现
typescript
// libs/langchain/src/agents/ReactAgent.ts:789-840
#createModelRouter(exitNode: string | typeof END = END) {
return (state: Record<string, unknown>) => {
const messages = state.messages;
const lastMessage = messages.at(-1);
// 检查是否有工具调用
if (!AIMessage.isInstance(lastMessage) ||
!lastMessage.tool_calls ||
lastMessage.tool_calls.length === 0) {
return exitNode; // 结束
}
// v2 模式:并行分发每个工具调用
const regularToolCalls = lastMessage.tool_calls.filter(
(toolCall) => !toolCall.name.startsWith("extract-")
);
return regularToolCalls.map(
(toolCall) => new Send(TOOLS_NODE_NAME, { ...state, lg_tool_call: toolCall })
);
};
}
jumpTo 机制:中间件可以控制流程跳转
typescript
// 中间件设置跳转目标
state.jumpTo = "tools"; // 强制调用工具
state.jumpTo = "model_request"; // 跳过工具
state.jumpTo = "__end__"; // 提前结束
// 路由器检查 jumpTo
if (state.jumpTo) {
const destination = parseJumpToTarget(state.jumpTo);
return new Send(destination, { ...state, jumpTo: undefined });
}
2.5 工具抽象:Strategy + Factory
核心文件 :libs/langchain-core/src/tools/index.ts
为什么需要工具抽象?
工具需要解决三个问题:
- 输入验证:LLM 可能生成格式错误的参数
- 统一输出:不同工具返回不同类型,需要标准化
- LLM 解耦:工具定义不应依赖特定 LLM
StructuredTool 类层次
typescript
// 基础抽象类
export abstract class StructuredTool<SchemaT> extends BaseLangChain {
abstract name: string;
abstract description: string; // LLM 用于理解工具用途
abstract schema: SchemaT; // Zod schema 或 JSON schema
// 子类实现具体逻辑
protected abstract _call(
arg: SchemaOutputT,
runManager?: CallbackManagerForToolRun
): Promise<ToolOutputT>;
// 统一调用接口(包含验证、回调)
async invoke(input: StructuredToolCallInput, config?: ToolRunnableConfig) {
// 1. 解析 ToolCall 或原始输入
// 2. Schema 验证
// 3. 执行回调
// 4. 格式化输出
}
}
// 字符串输入工具
export abstract class Tool extends StructuredTool {
schema = z.object({ input: z.string().optional() }).transform(obj => obj.input);
}
// 动态创建的结构化工具
export class DynamicStructuredTool extends StructuredTool {
name: string;
description: string;
func: (arg: SchemaOutputT, runManager?, config?) => Promise<ToolOutputT>;
schema: SchemaT;
}
Factory 函数
typescript
// libs/langchain-core/src/tools/index.ts:642-1025
export function tool(func, fields) {
const isSimpleStringSchema = isSimpleStringZodSchema(fields.schema);
// 简单字符串 schema → DynamicTool
if (!fields.schema || isSimpleStringSchema) {
return new DynamicTool({
...fields,
func: async (input, runManager, config) => {
return func(input, config);
}
});
}
// 对象 schema → DynamicStructuredTool
return new DynamicStructuredTool({
...fields,
func: async (input, runManager, config) => {
return func(input, config);
}
});
}
Schema 验证与错误处理
typescript
// libs/langchain-core/src/tools/index.ts:236-253
if (isInteropZodSchema(this.schema)) {
try {
parsed = await interopParseAsync(this.schema, inputForValidation);
} catch (e) {
let message = `Received tool input did not match expected schema`;
if (this.verboseParsingErrors) {
message = `${message}\nDetails: ${e.message}`;
}
throw new ToolInputParsingException(message, JSON.stringify(arg));
}
}
3. 实战案例:构建天气查询 Agent
3.1 完整代码
typescript
import { createAgent, tool } from "langchain";
import { z } from "zod";
// 1. 定义工具
const getWeather = tool(
async ({ city, unit = "celsius" }) => {
// 模拟 API 调用
const temperatures = {
paris: { celsius: 22, fahrenheit: 72 },
tokyo: { celsius: 28, fahrenheit: 82 },
"new york": { celsius: 18, fahrenheit: 64 }
};
const data = temperatures[city.toLowerCase()];
if (!data) {
return `Weather data not available for ${city}`;
}
const temp = unit === "celsius" ? data.celsius : data.fahrenheit;
return `Current weather in ${city}: ${temp}°${unit.charAt(0).toUpperCase()}, Sunny`;
},
{
name: "get_weather",
description: "Get current weather information for a specified city",
schema: z.object({
city: z.string().describe("The city name (e.g., Paris, Tokyo, New York)"),
unit: z.enum(["celsius", "fahrenheit"]).optional().default("celsius")
.describe("Temperature unit")
})
}
);
// 2. 创建 Agent
const weatherAgent = createAgent({
model: "openai:gpt-4o",
tools: [getWeather],
systemPrompt: `You are a helpful weather assistant. Use the get_weather tool to provide accurate weather information. If the tool doesn't have data, let the user know politely.`
});
// 3. 调用 Agent
const result = await weatherAgent.invoke({
messages: [{ role: "user", content: "What's the weather in Paris and Tokyo?" }]
});
console.log(result.messages.at(-1).content);
// "Paris is currently 22°C and sunny, while Tokyo is 28°C and sunny."
3.2 执行流程分解
css
用户输入: "What's the weather in Paris and Tokyo?"
↓
Agent 调用 model_request 节点
↓
GPT-4o 返回 AIMessage:
content: "Let me check the weather for both cities."
tool_calls: [
{ name: "get_weather", args: { city: "Paris", unit: "celsius" } },
{ name: "get_weather", args: { city: "Tokyo", unit: "celsius" } }
]
↓
Router 检测到 tool_calls,分发到 tools 节点(并行执行)
↓
ToolNode 执行两个工具调用:
- get_weather({ city: "Paris" }) → "Current weather in Paris: 22°C, Sunny"
- get_weather({ city: "Tokyo" }) → "Current weather in Tokyo: 28°C, Sunny"
↓
生成 ToolMessage 并添加到消息列表
↓
路由回到 model_request 节点
↓
GPT-4o 接收完整上下文,生成最终回答:
content: "Paris is currently 22°C and sunny, while Tokyo is 28°C and sunny."
tool_calls: [] (空,表示完成)
↓
Router 检测到无 tool_calls,路由到 END
↓
返回最终结果
4. 调用栈级别时序图
4.1 Agent 完整调用流程
4.2 工具执行的回调生命周期
4.3 中间件包装链
[Auth, Retry, Cache] →
Auth(Retry(Cache(BaseHandler))) AgentNode->>AuthMiddleware: wrapModelCall(request, innerHandler) AuthMiddleware->>AuthMiddleware: 验证权限 alt 验证失败 AuthMiddleware-->>AgentNode: Command { goto: END } else 验证通过 AuthMiddleware->>RetryMiddleware: wrapModelCall(request, innerHandler) RetryMiddleware->>CacheMiddleware: wrapModelCall(request, innerHandler) CacheMiddleware->>CacheMiddleware: 检查缓存 alt 缓存命中 CacheMiddleware-->>RetryMiddleware: AIMessage (缓存结果) else 缓存未命中 CacheMiddleware->>BaseHandler: wrapModelCall(request) BaseHandler->>ChatModel: invoke(messages) ChatModel-->>BaseHandler: AIMessage BaseHandler-->>CacheMiddleware: AIMessage CacheMiddleware->>CacheMiddleware: 存储缓存 end CacheMiddleware-->>RetryMiddleware: AIMessage alt 执行失败 RetryMiddleware->>RetryMiddleware: 重试逻辑 RetryMiddleware->>BaseHandler: 再次调用 end RetryMiddleware-->>AuthMiddleware: AIMessage AuthMiddleware->>AuthMiddleware: 记录审计日志 end AuthMiddleware-->>AgentNode: AIMessage
5. 设计模式深度解析
5.1 Runnable 接口:解决 API 碎片化
问题陈述:
在 LLM 应用中,你需要组合多种组件:
- Prompt 模板 → 格式化输入
- Chat Model → 调用 LLM API
- Output Parser → 解析响应
- Tool → 执行外部操作
- Retriever → 检索文档
每个组件原本都有不同的调用方式,如何统一?
解决方案 :Runnable<RunInput, RunOutput, CallOptions>
typescript
// 统一接口
interface RunnableInterface<RunInput, RunOutput, CallOptions> {
invoke(input: RunInput, options?: Partial<CallOptions>): Promise<RunOutput>;
batch(inputs: RunInput[], options?: ...): Promise<RunOutput[]>;
stream(input: RunInput, options?: ...): Promise<IterableReadableStream<RunOutput>>;
transform(generator: AsyncGenerator<RunInput>, options?: ...): AsyncGenerator<RunOutput>;
}
设计优势:
-
泛型约束:类型安全地定义输入输出
typescript// ChatModel: Messages → Message class ChatModel extends Runnable<BaseLanguageModelInput, AIMessageChunk, BaseChatModelCallOptions> {} // OutputParser: Message → ParsedObject class OutputParser<T> extends Runnable<BaseMessage, T, RunnableConfig> {} -
管道组合 :通过
.pipe()连接任意 Runnabletypescriptconst chain = prompt.pipe(model).pipe(parser); // 类型推导:Messages → Message → ParsedObject -
装饰器模式:添加通用能力
typescriptrunnable.withRetry({ stopAfterAttempt: 3 }); runnable.withFallbacks([fallback1, fallback2]); runnable.withConfig({ tags: ["production"] });
5.2 观察者/回调系统:解决可观测性问题
问题陈述:
在深度组合的调用链中,如何让外部系统(LangSmith、日志、监控)感知内部执行?
传统方案:每层手动传递 logger
typescript
// ❌ 侵入式、难以维护
async invoke(input, logger) {
logger.info("Chain start");
const result = await this.llm.invoke(input, logger);
logger.info("Chain end");
}
解决方案:分层回调管理器
typescript
// 1. 定义回调接口
interface CallbackHandlerMethods {
handleChainStart?(chain: Serialized, inputs: ChainValues, runId: string): Promise<void>;
handleChainEnd?(outputs: ChainValues, runId: string): Promise<void>;
handleLLMStart?(llm: Serialized, prompts: string[], runId: string): Promise<void>;
handleLLMNewToken?(token: string, runId: string): Promise<void>;
handleLLMEnd?(output: LLMResult, runId: string): Promise<void>;
handleToolStart?(tool: Serialized, input: string, runId: string): Promise<void>;
handleToolEnd?(output: string, runId: string): Promise<void>;
}
// 2. 配置时创建管理器
const callbackManager = await CallbackManager.configure(
config.callbacks, // 调用时传入的 callbacks
this.callbacks, // 组件默认的 callbacks
config.tags,
this.tags,
config.metadata,
this.metadata,
{ verbose: this.verbose }
);
// 3. 触发回调
const runManagers = await callbackManager?.handleChainStart(
this.toJSON(),
inputs,
config.runId
);
// 4. 子组件继承父级 handlers
const childManager = runManager.getChild("llm");
关键设计:
- RunManager 继承 :
getChild()复制inheritableHandlers - 异步安全 :
consumeCallback()避免阻塞主流程 - 错误隔离 :handler 异常不影响主流程(除非
raiseError: true)
5.3 策略模式在工具中的应用
问题陈述:
工具需要:
- 接收 LLM 生成的参数(可能是错误格式)
- 执行特定逻辑(API 调用、计算、数据库查询)
- 返回标准化结果(ToolMessage)
如何在不耦合 LLM 的情况下实现?
解决方案 :StructuredTool 分层抽象
typescript
// 第一层:抽象定义
abstract class StructuredTool {
abstract name: string; // 工具标识
abstract description: string; // LLM 理解用途
abstract schema: SchemaT; // 参数验证规则
// 子类只关注核心逻辑
protected abstract _call(arg: SchemaOutputT): Promise<ToolOutputT>;
}
// 第二层:动态创建
class DynamicStructuredTool extends StructuredTool {
constructor(fields: {
name: string;
description: string;
schema: ZodObject;
func: (args) => Promise<any>;
}) {
super(fields);
this.func = fields.func;
}
protected _call(arg, runManager) {
return this.func(arg, runManager);
}
}
// 第三层:工厂函数
function tool(func, fields) {
return new DynamicStructuredTool({
...fields,
func: async (input, runManager, config) => {
return func(input, config);
}
});
}
设计优势:
- LLM 解耦:工具不知道也不关心哪个 LLM 调用它
- 验证隔离 :Schema 验证在
invoke()中统一处理 - 输出标准化 :
_formatToolOutput()自动包装为 ToolMessage
5.4 状态机在 Agent 中的创新
问题陈述:
Agent 需要灵活的控制流:
- 循环执行(LLM → Tool → LLM)
- 条件分支(有工具调用?→ 是/否)
- 中间件干预(提前终止、重试、跳转)
如何用声明式方式表达?
解决方案:LangGraph StateGraph
typescript
// 1. 定义状态 schema
const state = {
messages: Annotation({
reducer: (prev, next) => [...prev, ...next], // 消息累加
default: () => []
}),
structuredResponse: Annotation({
reducer: (prev, next) => next, // 覆盖
default: () => undefined
}),
jumpTo: Annotation({ // 中间件控制流
reducer: (prev, next) => next,
default: () => undefined
})
};
// 2. 构建图
const workflow = new StateGraph(state);
workflow.addNode("model_request", agentNode);
workflow.addNode("tools", toolNode);
workflow.addEdge(START, "model_request");
// 条件路由
workflow.addConditionalEdges("model_request", (state) => {
const lastMessage = state.messages.at(-1);
if (lastMessage.tool_calls?.length > 0) {
return "tools";
}
return END;
});
workflow.addEdge("tools", "model_request");
// 3. 编译为可执行图
const compiledGraph = workflow.compile();
创新点:jumpTo 机制
中间件可以动态改变流程:
typescript
// 缓存中间件
const cacheMiddleware = {
name: "cache",
afterModel: async (state, runtime) => {
const lastMessage = state.messages.at(-1);
if (lastMessage.content) {
// 缓存结果
await cache.set(getCacheKey(state), lastMessage.content);
}
},
beforeModel: async (state, runtime) => {
// 检查缓存
const cached = await cache.get(getCacheKey(state));
if (cached) {
// 跳过模型调用,直接返回缓存
return new Command({
update: {
messages: [new AIMessage({ content: cached })]
},
goto: "__end__" // 跳转到结束
});
}
}
};
6. 关键实现细节
6.1 流式架构
设计目标:原生支持流式输出,兼容 Web API
核心组件:
typescript
// 1. AsyncGenerator 作为基础抽象
async *_streamIterator(input, options) {
for await (const chunk of this._streamResponseChunks(messages, options)) {
yield chunk;
}
}
// 2. IterableReadableStream 桥接 Web ReadableStream
async stream(input, options): Promise<IterableReadableStream<RunOutput>> {
const wrappedGenerator = new AsyncGeneratorWithSetup({
generator: this._streamIterator(input, config),
config,
});
await wrappedGenerator.setup; // 等待第一个 chunk(处理初始错误)
return IterableReadableStream.fromAsyncGenerator(wrappedGenerator);
}
// 3. 流转换管道
async *_transformStreamWithConfig(
inputGenerator: AsyncGenerator<I>,
transformer: (generator: AsyncGenerator<I>, runManager?) => AsyncGenerator<O>,
options?: Partial<CallOptions>
): AsyncGenerator<O> {
// 回调管理、错误处理、信号支持
}
使用示例:
typescript
// LangChain 流式调用
const stream = await agent.stream({
messages: [{ role: "user", content: "Hello" }]
}, { streamMode: "values" });
for await (const chunk of stream) {
console.log(chunk.messages.at(-1).content);
}
// 转换为 HTTP SSE
const sseStream = stream.pipeThrough(new TextEncoderStream());
return new Response(sseStream, {
headers: { "Content-Type": "text/event-stream" }
});
6.2 错误处理与重试
装饰器模式实现重试:
typescript
// libs/langchain-core/src/runnables/base.ts:156-168
withRetry(fields?: {
stopAfterAttempt?: number;
onFailedAttempt?: RunnableRetryFailedAttemptHandler;
}): RunnableRetry<RunInput, RunOutput, CallOptions> {
return new RunnableRetry({
bound: this,
kwargs: {},
config: {},
maxAttemptNumber: fields?.stopAfterAttempt,
...fields,
});
}
// RunnableRetry 内部使用 p-retry
class RunnableRetry extends Runnable {
async invoke(input, options) {
return pRetry(
async (attemptNumber) => {
return this.bound.invoke(input, options);
},
{
retries: this.maxAttemptNumber - 1,
onFailedAttempt: this.onFailedAttempt,
}
);
}
}
降级方案(Chain of Responsibility):
typescript
withFallbacks(fallbacks: Runnable[]): RunnableWithFallbacks {
return new RunnableWithFallbacks({
runnable: this,
fallbacks,
});
}
class RunnableWithFallbacks extends Runnable {
async invoke(input, options) {
try {
return await this.runnable.invoke(input, options);
} catch (e) {
// 依次尝试每个降级方案
for (const fallback of this.fallbacks) {
try {
return await fallback.invoke(input, options);
} catch (fallbackError) {
// 继续下一个
}
}
throw e; // 全部失败
}
}
}
AbortSignal 集成:
typescript
// 全局支持取消操作
const signal = AbortSignal.timeout(30000); // 30秒超时
const result = await agent.invoke(input, { signal });
// 内部实现
async *_streamIterator(input, options) {
options.signal?.throwIfAborted(); // 检查是否已取消
for await (const chunk of this._streamResponseChunks(...)) {
options.signal?.throwIfAborted(); // 每个 chunk 都检查
yield chunk;
}
}
6.3 配置管理
统一配置对象:
typescript
interface RunnableConfig {
tags?: string[]; // 标签过滤
metadata?: Record<string, unknown>; // 元数据
callbacks?: Callbacks; // 回调处理器
runId?: string; // 追踪 ID
recursionLimit?: number; // 递归限制
maxConcurrency?: number; // 并发控制
signal?: AbortSignal; // 取消信号
timeout?: number; // 超时时间
configurable?: Record<string, any>; // 动态配置(LangGraph 用)
}
配置合并策略:
typescript
// 合并配置(调用时覆盖默认值)
function mergeConfigs(...configs: (RunnableConfig | undefined)[]): RunnableConfig {
const merged: RunnableConfig = {};
for (const config of configs) {
if (!config) continue;
// tags 合并(去重)
if (config.tags) {
merged.tags = [...new Set([...(merged.tags || []), ...config.tags])];
}
// metadata 深度合并
if (config.metadata) {
merged.metadata = { ...merged.metadata, ...config.metadata };
}
// callbacks 追加
if (config.callbacks) {
merged.callbacks = [...(merged.callbacks || []), ...config.callbacks];
}
// 其他字段覆盖
Object.assign(merged, config);
}
return merged;
}
// 部分更新配置
function patchConfig(config: RunnableConfig, updates: Partial<RunnableConfig>): RunnableConfig {
return {
...config,
...updates,
callbacks: updates.callbacks || config.callbacks,
};
}
AsyncLocalStorage 隐式传递:
typescript
// 在 Node.js 环境中自动传递配置
import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons";
// 工具调用时自动获取上下文
async invoke(input, config) {
return AsyncLocalStorageProviderSingleton.runWithConfig(
pickRunnableConfigKeys(config),
async () => {
// 内部可以隐式获取 config
return this.func(input);
}
);
}
7. 总结
7.1 架构演进启示
LangChain.js 的架构经历了三个阶段:
- Chain 时代:硬编码组合,缺乏灵活性
- LCEL (LangChain Expression Language):引入 Runnable,实现声明式组合
- Agent + LangGraph:状态机驱动,支持复杂控制流
每次演进都围绕一个核心目标:降低复杂性,提升可组合性。
7.2 为什么这些设计模式重要?
| 模式 | 解决的问题 | LLM 应用中的价值 |
|---|---|---|
| Runnable | API 碎片化 | 统一模型、工具、链的调用方式 |
| Observer/Callback | 可观测性缺失 | 追踪任意深度的调用链 |
| Strategy | 组件差异 | LLM 提供商可插拔 |
| State Machine | 控制流复杂 | Agent 循环、条件路由、中间件 |
| Decorator | 横切关注点 | 重试、缓存、认证无侵入添加 |
7.3 设计原则回顾
scss
┌─────────────────────────────────────────────────────┐
│ LangChain.js 设计原则 │
├─────────────────────────────────────────────────────┤
│ 1. 单一抽象层:Runnable 统一所有组件 │
│ 2. 组合优于继承:.pipe() 替代深类层次 │
│ 3. 依赖注入:通过 Config 传递横切关注点 │
│ 4. 流式优先:AsyncGenerator 原生支持 │
│ 5. 可观测性内置:回调系统无需手动埋点 │
│ 6. 中间件扩展:开闭原则,对扩展开放 │
│ 7. 类型安全:TypeScript 泛型确保编译期检查 │
└─────────────────────────────────────────────────────┘
7.4 扩展而不复杂
LangChain.js 的架构哲学可以总结为一句话:
通过组合简单原语构建复杂系统,而非通过复杂原语构建简单系统
- ✅ 简单原语:
Runnable.invoke()、tool(func)、middleware.hook() - ✅ 组合方式:
.pipe()、createAgent()、StateGraph - ❌ 避免:为每个场景创建专用 API
这种设计让开发者可以:
- 快速原型:几行代码组装 Agent
- 渐进增强:添加中间件、回调、流式处理
- 深度定制:继承
Runnable、实现自定义节点
附录:关键文件索引
| 模块 | 文件路径 | 核心类/函数 |
|---|---|---|
| Runnable 基类 | libs/langchain-core/src/runnables/base.ts |
Runnable, RunnableLambda, RunnableSequence |
| Chat Model | libs/langchain-core/src/language_models/chat_models.ts |
BaseChatModel |
| Tool 系统 | libs/langchain-core/src/tools/index.ts |
StructuredTool, DynamicTool, tool() |
| 回调系统 | libs/langchain-core/src/callbacks/manager.ts |
CallbackManager, BaseRunManager |
| Agent | libs/langchain/src/agents/ReactAgent.ts |
ReactAgent, createAgent |
| Agent Node | libs/langchain/src/agents/nodes/AgentNode.ts |
AgentNode |
| 流式工具 | libs/langchain-core/src/utils/stream.ts |
IterableReadableStream, concat |