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: 从后向前包装
[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>;
}

设计优势

  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
相关推荐
百度Geek说1 小时前
我把 Karpathy 的 AutoResearch 搬到了软件开发领域,效果炸了
人工智能
俺爱吃萝卜2 小时前
Spring Boot 3 + JDK 17:新一代微服务架构最佳实践
java·spring boot·架构
嵌入式小企鹅2 小时前
国产大模型与芯片加速融合,RISC-V生态多点开花,AI编程工具迈入自动化新纪元
人工智能·学习·ai·嵌入式·算力·risc-v·半导体
数智大号2 小时前
聚焦 AI 音频创新 ,Shure 亮相 InfoComm 全场景解决方案破解协作难题
人工智能
光影少年2 小时前
Monorepo架构是什么,如何学习Monorepo架构?
前端·学习·架构·前端框架
做个文艺程序员2 小时前
Spring Boot 项目集成 OpenClAW【OpenClAW + Spring Boot 系列 第1篇】
java·人工智能·spring boot·开源
天一生水water2 小时前
CNN循环神经网络关键知识点
人工智能·rnn·cnn
一个喜欢分享的PHP技术2 小时前
AI在龙虾中,配置标准版mcp的方法
人工智能
醇氧2 小时前
Hermes Agent 学习(安装部署详细教程)
人工智能·python·学习·阿里云·ai·云计算