LangChain.js 架构设计深度剖析

目录

  1. [引言:为什么需要 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")
  2. 核心架构设计哲学
  3. [实战案例:构建天气查询 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")
  4. 调用栈级别时序图
  5. 设计模式深度解析
  6. 关键实现细节
  7. 总结

1. 引言:为什么需要 LangChain.js

1.1 LLM 生态的碎片化问题

在 LangChain.js 出现之前,开发者面临三大挑战:

  1. API 碎片化:每个 LLM 提供商(OpenAI、Anthropic、Google)都有独特的 API 设计
  2. 组合困难:Prompt、模型、工具、输出解析器之间缺乏统一的组合方式
  3. 可观测性缺失:复杂的调用链难以追踪和调试

1.2 核心设计哲学:"Everything is a Runnable"

LangChain.js 的核心洞察是:所有 LLM 操作都可以抽象为"输入→输出"的转换过程

ini 复制代码
Prompt + LLM + Tools + Output Parser = 一个可组合的 Runnable

这种统一抽象带来三大优势:

  • 可组合性(Composability):任意 Runnable 可以像管道一样连接
  • 互操作性(Interoperability):不同提供商的组件可以无缝替换
  • 可观测性(Observability):统一的回调系统追踪整个调用链

1.3 设计原则

原则 说明 实现方式
单一职责 每个模块只做一件事 RunnableToolMessage 分离
开闭原则 对扩展开放,对修改封闭 中间件系统、回调系统
依赖倒置 依赖抽象而非具体实现 RunnableInterfaceStructuredToolInterface
流式优先 原生支持流式处理 AsyncGeneratorIterableReadableStream

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

为什么需要工具抽象?

工具需要解决三个问题:

  1. 输入验证:LLM 可能生成格式错误的参数
  2. 统一输出:不同工具返回不同类型,需要标准化
  3. 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 完整调用流程

sequenceDiagram participant User participant ReactAgent participant StateGraph participant AgentNode participant ChatModel participant Router participant ToolNode participant StructuredTool participant CallbackManager User->>ReactAgent: invoke({ messages: [...] }) ReactAgent->>StateGraph: invoke(initialState, config) StateGraph->>CallbackManager: handleChainStart(agent) StateGraph->>AgentNode: execute(state) AgentNode->>CallbackManager: handleChainStart(model_request) AgentNode->>ChatModel: invoke(messages, { tools: [...] }) ChatModel->>CallbackManager: handleLLMStart(model, messages) ChatModel->>ChatModel: _streamResponseChunks() loop 流式输出 ChatModel->>CallbackManager: handleLLMNewToken(token) end ChatModel->>CallbackManager: handleLLMEnd(result) ChatModel-->>AgentNode: AIMessage { tool_calls: [...] } AgentNode->>CallbackManager: handleChainEnd(model_request) AgentNode-->>StateGraph: Command { messages: [AIMessage] } StateGraph->>Router: route(state) Router->>Router: 检查 lastMessage.tool_calls alt 有工具调用 Router->>ToolNode: Send(TOOLS_NODE_NAME, { lg_tool_call }) ToolNode->>CallbackManager: handleChainStart(tools) loop 每个 tool_call ToolNode->>StructuredTool: invoke(toolCall, config) StructuredTool->>CallbackManager: handleToolStart(tool, input) StructuredTool->>StructuredTool: _call(parsedArgs) StructuredTool-->>StructuredTool: 执行工具逻辑 StructuredTool->>CallbackManager: handleToolEnd(output) StructuredTool-->>ToolNode: ToolMessage end ToolNode->>CallbackManager: handleChainEnd(tools) ToolNode-->>StateGraph: Command { messages: [ToolMessage] } StateGraph->>Router: route(state) Router->>Router: 检查是否还有 tool_calls alt 还有 tool_calls Router->>AgentNode: 回到 model_request(循环) else 无 tool_calls Router->>StateGraph: 路由到 END end else 无工具调用 Router->>StateGraph: 路由到 END end StateGraph->>CallbackManager: handleChainEnd(agent) StateGraph-->>ReactAgent: finalState ReactAgent-->>User: { messages: [...], structuredResponse: ... }

4.2 工具执行的回调生命周期

sequenceDiagram participant ToolNode participant CallbackManager participant StructuredTool participant ToolFunction participant RunManager ToolNode->>StructuredTool: invoke(toolCall, config) StructuredTool->>CallbackManager: configure(callbacks) CallbackManager-->>StructuredTool: callbackManager_ StructuredTool->>callbackManager_: handleToolStart(tool, input, runId) callbackManager_->>RunManager: 创建 RunManager StructuredTool->>StructuredTool: interopParseAsync(schema, input) alt Schema 验证失败 StructuredTool->>RunManager: handleToolError(exception) StructuredTool-->>ToolNode: 抛出 ToolInputParsingException else Schema 验证成功 StructuredTool->>ToolFunction: _call(parsedArgs, runManager) alt 同步返回 ToolFunction-->>StructuredTool: result StructuredTool->>RunManager: handleToolEnd(formattedOutput) StructuredTool-->>ToolNode: ToolMessage | result else 流式返回 (AsyncGenerator) loop 每个 chunk ToolFunction->>StructuredTool: yield event StructuredTool->>RunManager: handleToolEvent(event) end ToolFunction-->>StructuredTool: final result StructuredTool->>RunManager: handleToolEnd(formattedOutput) StructuredTool-->>ToolNode: ToolMessage | result end end

4.3 中间件包装链

sequenceDiagram participant AgentNode participant AuthMiddleware participant RetryMiddleware participant CacheMiddleware participant BaseHandler participant ChatModel AgentNode->>AgentNode: 构建中间件链 Note over AgentNode: 从后向前包装<br/>[Auth, Retry, Cache] →<br/>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>;
}

设计优势

  1. 泛型约束:类型安全地定义输入输出

    typescript 复制代码
    // ChatModel: Messages → Message
    class ChatModel extends Runnable<BaseLanguageModelInput, AIMessageChunk, BaseChatModelCallOptions> {}
    
    // OutputParser: Message → ParsedObject
    class OutputParser<T> extends Runnable<BaseMessage, T, RunnableConfig> {}
  2. 管道组合 :通过 .pipe() 连接任意 Runnable

    typescript 复制代码
    const chain = prompt.pipe(model).pipe(parser);
    // 类型推导:Messages → Message → ParsedObject
  3. 装饰器模式:添加通用能力

    typescript 复制代码
    runnable.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 策略模式在工具中的应用

问题陈述

工具需要:

  1. 接收 LLM 生成的参数(可能是错误格式)
  2. 执行特定逻辑(API 调用、计算、数据库查询)
  3. 返回标准化结果(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 的架构经历了三个阶段:

  1. Chain 时代:硬编码组合,缺乏灵活性
  2. LCEL (LangChain Expression Language):引入 Runnable,实现声明式组合
  3. 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
相关推荐
Coffeeee16 分钟前
帮你快速理解AI Agent之我想招个Android实习生
android·人工智能·agent
新新技术迷23 分钟前
AI聊天自动跟随滚动,附回到底部按钮
人工智能
先锋部队23 分钟前
用Web Worker解析AI返回的大文本不卡UI
人工智能
把你拉进白名单27 分钟前
8.OpenClaw源码解析——三层洋葱重试
人工智能·llm·agent
用户6324150317829 分钟前
拖文档进AI对话框解析,前端要处理哪些脏活
人工智能
姗姗来迟了36 分钟前
AI回答里的引用来源卡片,前端怎么做
人工智能
用户71062077334037 分钟前
Codex-端口配置错误排查案例(stream disconnected before completion)
人工智能
IT_陈寒2 小时前
JavaScript的默认参数挖坑实录,我掉进去了
前端·人工智能·后端
米小虾2 小时前
多Agent系统编排详解:从架构设计到代码实现
人工智能·agent