第 16 课: 第一条完整 Chain — 组合实战

0 阅读6分钟

课程目标

将前面学过的所有核心组件(Prompt、Model、Parser、Tools、History)组合成完整的应用链。掌握链的类型匹配、流式执行、错误处理与优雅降级策略。


16.1 组件回顾与类型链路

在前面的课程中,我们已经学习了所有核心组件。现在看它们在链中的类型流转:

ChatPromptTemplate          Record<string, any>  →  ChatPromptValue
  .pipe(ChatModel)          ChatPromptValueAIMessage
  .pipe(StringOutputParser) AIMessagestring

每个组件都是 Runnable,pipe() 将它们串联成 RunnableSequence(第 5 课),泛型保证类型安全:

// 链的完整类型签名
RunnableSequence<Record<string, any>, string>

16.2 基础链:Prompt + Model + Parser

16.2.1 最简单的链

import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful translator. Translate the following to {language}."],
  ["human", "{text}"],
]);

const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const parser = new StringOutputParser();

// 用 pipe 串联
const chain = prompt.pipe(model).pipe(parser);

// invoke:一次性获取完整结果
const result = await chain.invoke({
  language: "Chinese",
  text: "Hello, how are you?",
});
// "你好,你好吗?"

16.2.2 结构化输出链

import { JsonOutputParser } from "@langchain/core/output_parsers";

interface TranslationResult {
  translation: string;
  confidence: number;
  alternatives: string[];
}

const structuredPrompt = ChatPromptTemplate.fromMessages([
  ["system", `You are a translator. Respond in JSON with fields:
- translation: the translated text
- confidence: confidence score 0-1
- alternatives: array of alternative translations`],
  ["human", "Translate to {language}: {text}"],
]);

const structuredChain = structuredPrompt
  .pipe(model)
  .pipe(new JsonOutputParser<TranslationResult>());

const result = await structuredChain.invoke({
  language: "Japanese",
  text: "Good morning",
});
// { translation: "おはようございます", confidence: 0.95, alternatives: ["おはよう", "..."] }

16.3 带工具的链

16.3.1 Model + bindTools

import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages";

// 定义工具
const weatherTool = tool(
  async ({ city }) => {
    // 实际项目中调用天气 API
    const data: Record<string, string> = {
      "Beijing": "25°C, Sunny",
      "Tokyo": "22°C, Cloudy",
      "London": "15°C, Rainy",
    };
    return data[city] ?? "Weather data not available";
  },
  {
    name: "get_weather",
    description: "Get the current weather for a city",
    schema: z.object({
      city: z.string().describe("The city name in English"),
    }),
  }
);

const calculatorTool = tool(
  async ({ expression }) => {
    try {
      return String(eval(expression));
    } catch {
      return "Invalid expression";
    }
  },
  {
    name: "calculator",
    description: "Calculate a math expression",
    schema: z.object({
      expression: z.string().describe("A math expression"),
    }),
  }
);

// 绑定工具到模型
const modelWithTools = model.bindTools([weatherTool, calculatorTool]);

16.3.2 完整的工具调用循环

async function runWithTools(question: string): Promise<string> {
  const tools = [weatherTool, calculatorTool];
  const toolMap = Object.fromEntries(tools.map((t) => [t.name, t]));

  let messages = [new HumanMessage(question)];

  // 循环直到模型不再调用工具
  while (true) {
    const response = await modelWithTools.invoke(messages);
    messages.push(response);

    // 没有工具调用,直接返回文本回复
    if (!response.tool_calls || response.tool_calls.length === 0) {
      return typeof response.content === "string"
        ? response.content
        : JSON.stringify(response.content);
    }

    // 执行工具调用
    for (const toolCall of response.tool_calls) {
      const selectedTool = toolMap[toolCall.name];
      if (selectedTool) {
        const toolResult = await selectedTool.invoke(toolCall);
        messages.push(toolResult);
      }
    }
  }
}

const answer = await runWithTools(
  "What's the weather in Beijing? Also, what is 15 * 37?"
);

16.4 带对话历史的链

结合第 10 课的 RunnableWithMessageHistory

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { ChatMessageHistory } from "langchain/stores/message/in_memory";

// 1. 创建带历史插槽的 Prompt
const prompt = ChatPromptTemplate.fromMessages([
  ["system", "You are a helpful assistant. Answer concisely."],
  new MessagesPlaceholder("chat_history"),
  ["human", "{input}"],
]);

const chain = prompt.pipe(model).pipe(new StringOutputParser());

// 2. 用 Map 管理每个 session 的历史
const messageHistories = new Map<string, ChatMessageHistory>();

const getMessageHistory = (sessionId: string) => {
  if (!messageHistories.has(sessionId)) {
    messageHistories.set(sessionId, new ChatMessageHistory());
  }
  return messageHistories.get(sessionId)!;
};

// 3. 用 RunnableWithMessageHistory 包装
const chainWithHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory,
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

// 4. 使用——同一 session 自动维护上下文
const config = { configurable: { sessionId: "user-001" } };

const r1 = await chainWithHistory.invoke({ input: "My name is Alice" }, config);
// "Nice to meet you, Alice!"

const r2 = await chainWithHistory.invoke({ input: "What is my name?" }, config);
// "Your name is Alice."

// 不同 session 互不影响
const r3 = await chainWithHistory.invoke(
  { input: "What is my name?" },
  { configurable: { sessionId: "user-002" } }
);
// "I don't know your name yet."

16.5 链的流式执行

16.5.1 chain.stream()

const chain = prompt.pipe(model).pipe(new StringOutputParser());

// stream() 返回 AsyncGenerator,逐 chunk 输出
const stream = await chain.stream({
  language: "Chinese",
  text: "Tell me a story",
});

for await (const chunk of stream) {
  process.stdout.write(chunk); // 逐 token 输出
}

流式传播原理(第 5 课 RunnableSequence):

Prompt.invoke() → 完整的 ChatPromptValue
  → Model._streamIterator() → AIMessageChunk 流
    → Parser.transform() → string chunk 流

16.5.2 chain.streamEvents()

streamEvents 提供更细粒度的事件流,能观察到链中每个节点的执行状态:

const eventStream = chain.streamEvents(
  { language: "Chinese", text: "Hello" },
  { version: "v2" }
);

for await (const event of eventStream) {
  if (event.event === "on_llm_stream") {
    // LLM 输出的每个 token
    process.stdout.write(event.data.chunk.content);
  } else if (event.event === "on_chain_start") {
    console.log(`Chain started: ${event.name}`);
  } else if (event.event === "on_chain_end") {
    console.log(`Chain ended: ${event.name}`);
  }
}

常见事件类型:

事件触发时机
on_chain_start / on_chain_end链/子链 开始/结束
on_llm_start / on_llm_endLLM 调用 开始/结束
on_llm_streamLLM 输出每个 token
on_tool_start / on_tool_end工具调用 开始/结束
on_prompt_start / on_prompt_endPrompt 格式化 开始/结束
on_parser_start / on_parser_endParser 解析 开始/结束

16.6 withRetry — 自动重试

// 为整条链添加重试策略
const robustChain = chain.withRetry({
  stopAfterAttempt: 3,  // 最多重试 3 次
  onFailedAttempt: (error, attempt) => {
    console.log(`Attempt ${attempt} failed: ${error.message}`);
  },
});

// 也可以只为特定节点添加重试
const robustModel = model.withRetry({ stopAfterAttempt: 2 });
const chain = prompt.pipe(robustModel).pipe(parser);

16.7 withFallbacks — 降级方案

import { ChatAnthropic } from "@langchain/anthropic";

const primaryModel = new ChatOpenAI({ model: "gpt-4o" });
const fallbackModel = new ChatAnthropic({ model: "claude-sonnet-4-20250514" });

// 主模型失败时自动切换到备用模型
const reliableModel = primaryModel.withFallbacks({
  fallbacks: [fallbackModel],
});

const chain = prompt.pipe(reliableModel).pipe(parser);

16.8 withConfig — 运行时配置

// 为链绑定默认配置
const configuredChain = chain.withConfig({
  tags: ["production", "translation"],
  metadata: { version: "1.0" },
  runName: "TranslationChain",
});

16.9 实战模式汇总

模式 1:问答链

const qaChain = ChatPromptTemplate.fromMessages([
  ["system", "Answer the question based on the context.\n\nContext: {context}"],
  ["human", "{question}"],
])
  .pipe(model)
  .pipe(new StringOutputParser());

模式 2:提取链

const extractChain = ChatPromptTemplate.fromMessages([
  ["system", "Extract structured information from the text. Respond in JSON."],
  ["human", "{text}"],
])
  .pipe(model)
  .pipe(new JsonOutputParser());

模式 3:翻译链

const translateChain = ChatPromptTemplate.fromMessages([
  ["system", "Translate the following text to {target_language}. Only output the translation."],
  ["human", "{text}"],
])
  .pipe(model)
  .pipe(new StringOutputParser());

16.10 综合实战:智能客服链

import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { RunnablePassthrough, RunnableParallel } from "@langchain/core/runnables";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { ChatMessageHistory } from "langchain/stores/message/in_memory";

// 1. 定义输出格式
interface CustomerServiceResponse {
  answer: string;
  confidence: number;
  sources: string[];
  needsHumanReview: boolean;
}

// 2. 模拟知识库检索
async function searchFAQ(question: string): Promise<string> {
  const faq: Record<string, string> = {
    "refund": "Refunds are processed within 7 business days.",
    "shipping": "Standard shipping takes 3-5 business days.",
    "return": "Items can be returned within 30 days of purchase.",
  };
  const key = Object.keys(faq).find((k) => question.toLowerCase().includes(k));
  return key ? faq[key] : "No FAQ found for this question.";
}

// 3. 构建并行检索 + 问题传递
const contextRetriever = RunnableParallel.from({
  faq: async (input: { input: string }) => searchFAQ(input.input),
  question: (input: { input: string }) => input.input,
});

// 4. 构建 Prompt
const prompt = ChatPromptTemplate.fromMessages([
  ["system", `You are a customer service assistant. Use the FAQ information to answer.
Always respond in JSON with: answer, confidence (0-1), sources (array), needsHumanReview (boolean).

FAQ Information:
{faq}`],
  new MessagesPlaceholder({ variableName: "chat_history", optional: true }),
  ["human", "{question}"],
]);

// 5. 组合完整链
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const parser = new JsonOutputParser<CustomerServiceResponse>();

const coreChain = contextRetriever
  .pipe(prompt)
  .pipe(model)
  .pipe(parser);

// 6. 添加对话历史
const histories = new Map<string, ChatMessageHistory>();

const fullChain = new RunnableWithMessageHistory({
  runnable: coreChain,
  getMessageHistory: (sessionId: string) => {
    if (!histories.has(sessionId)) {
      histories.set(sessionId, new ChatMessageHistory());
    }
    return histories.get(sessionId)!;
  },
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

// 7. 添加重试和降级
const robustChain = fullChain.withRetry({ stopAfterAttempt: 2 });

// 8. 使用
const config = { configurable: { sessionId: "customer-001" } };

const response = await robustChain.invoke(
  { input: "How long does shipping take?" },
  config
);
// {
//   answer: "Standard shipping takes 3-5 business days.",
//   confidence: 0.95,
//   sources: ["FAQ"],
//   needsHumanReview: false
// }

// 流式执行,观察中间过程
const events = robustChain.streamEvents(
  { input: "What about returns?" },
  { version: "v2", ...config }
);

for await (const event of events) {
  if (event.event === "on_parser_end") {
    console.log("Parsed result:", event.data.output);
  }
}

16.11 链的错误传播

RunnableSequence 中,错误会沿链传播并中断执行。处理策略:

策略方法适用场景
自动重试withRetry()网络抖动、API 限流
降级备选withFallbacks()Provider 故障
错误捕获try-catch + OutputParserException输出格式错误
默认值RunnableLambda 包装非关键节点失败

16.12 源码精读路线

优先级文件关注点
P0runnables/base.tsRunnableSequence——理解 pipe() 串联的内部结构
P0runnables/base.tsstream(), _streamIterator() 流式传播
P1runnables/config.tsRunnableConfig 在链中的传递
P1runnables/base.tswithRetry(), withFallbacks()
P2runnables/passthrough.tsRunnablePassthrough.assign() 数据编排

本课收获总结

级别你应该掌握的
🟢 基础完成第一个从 Prompt → Model → Parser 的完整链,理解 invoke 和 stream
🔵 中阶理解链中每个节点的输入输出类型如何匹配;掌握 RunnableWithMessageHistory 包装
🟡 高阶掌握 streamEvents 观察链的完整执行过程;理解流式传播原理
🟠 资深分析链的错误传播机制;设计 withRetry + withFallbacks 的优雅降级策略
🔴 架构建立"组件化 AI 应用"的系统思维:Prompt 模块化 + Model 可替换 + Parser 可插拔 + 工具可扩展

下一课预告

第 17 课讲上下文变量、单例系统与错误类型——Callbacks 是怎么在深层嵌套的 Runnable 链中传播的?框架的错误为什么有结构?