Agent开发 - LangChain 核心五要素从 Runnable 到 Messages

文章基于自己的学习笔记整理而成,主要围绕 LangChain 里五个最常用的概念:Runnable、invoke、tools、tool_calls 和 messages,语言示例以typescript为主

一、Runnable

前 LCEL 时代的问题

在 LangChain 1.0 之前,各个组件的调用方式不统一。每个模块都有自己的写法:

typescript 复制代码
const model = new OpenAI({ temperature: 0.9 });
const prompt = new PromptTemplate({ ... });

const llmResult = await model.call(prompt);      // 模型用 .call()
const chainResult = await chain.run(input);      // 链条用 .run()
const toolResult = await tool.invoke(args);      // 工具又是 .invoke()

这种写法除了别扭之外,最大的问题是难以组合。想串起多个步骤就得手动嵌套代码,写出来基本就是回调地狱。

Runnable 统一标准

LangChain 1.0 引入 Runnable 协议来解决这个问题。可以把它理解成一个"万能插头规范"------只要一个类实现了 Runnable 接口,就自动拥有了 .invoke().stream().batch() 这三个标准方法。

typescript 复制代码
import { RunnableLambda } from "@langchain/core/runnables";

async function customProcess(input: string): Promise<string> {
  return `Processed: ${input}`;
}

const runnableFunc = RunnableLambda.from(customProcess);

const result = await runnableFunc.invoke("Hello");
console.log(result); // Processed: Hello

现在不管是模型、提示词模板还是自定义函数,调用方式全统一了。

LCEL:用管道搭建应用

有了 Runnable 标准之后,LCEL(LangChain 表达式语言)就出来了。核心是通过 .pipe() 方法把 Runnable 组件串起来,上一个的输出自动成为下一个的输入。

最经典的例子:

typescript 复制代码
import { PromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = PromptTemplate.fromTemplate("用一句话解释什么是 {topic}");
const model = new ChatOpenAI({ temperature: 0 });
const parser = new StringOutputParser();

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

const result = await chain.invoke({ topic: "递归" });
console.log(result);
// "递归是一种在函数定义中调用自身的方法。"

更复杂的场景也支持,比如并行执行和条件分支:

typescript 复制代码
import { RunnableParallel, RunnableBranch } from "@langchain/core/runnables";

// 并行执行两个任务
const parallelChain = RunnableParallel.from({
  weather: promptWeather.pipe(model),
  news: promptNews.pipe(model)
});
const parallelResult = await parallelChain.invoke({ city: "Beijing" });

// 条件分支
const branchChain = new RunnableBranch([
  (input) => input.includes("天气"), weatherChain,
  (input) => input.includes("新闻"), newsChain,
  defaultChain
]);

用 Web 开发类比一下

  • Runnable 就像定义了 request/response 规范的 HTTP 接口,不管是 /user 还是 /order,交互方式都一样。
  • LCEL(.pipe())就像 Node.js 的 ReadableStream.pipe(),把数据处理环节串起来。
  • .invoke() 就像 Express 里的 app.handleRequest(),触发整个管线开始工作。

二、invoke

原生 API 里的 invoke

直接调用 OpenAI、Anthropic 这些大模型 API 时,你通常会看到 .create().chat.completions.create() 这样的方法。不过很多封装库和本地模型(比如 Ollama)会用 invoke

javascript 复制代码
response = llm.invoke([
  { role: "user", content: "你好!" }
]);
console.log(response);

这里的 invoke 就是用户端和模型端之间的那个"连接动作",代表一次完整的请求到响应的过程。

LangChain 里的 invoke

在 LangChain 中,invoke 是一个统一的标准接口。几乎任何"可以运行"的东西------模型、工具、检索器、提示词模板、整个链条------都有 .invoke() 方法。

为什么统一用 invoke?因为这样你就能用同样的方式调用极其简单的组件和极其复杂的管线,不用关心背后是简单的字符串拼接还是包含几十次调用的复杂工作流。

不同组件用 invoke 时做的事情不一样:

组件 输入 invoke 做的事情 输出
PromptTemplate { "topic": "AI" } 填充模板生成字符串 字符串
ChatModel 消息列表 发给大模型 API 等回复 AIMessage
Tool 参数字典 执行绑定的本地函数 函数执行结果
Chain 初始输入 让输入依次流过每个组件 最终结果

实际例子:

typescript 复制代码
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = ChatPromptTemplate.fromTemplate("讲一个关于 {topic} 的短笑话");
const model = new ChatOpenAI({ model: "gpt-3.5-turbo", temperature: 0.7 });
const outputParser = new StringOutputParser();

const chain = prompt.pipe(model).pipe(outputParser);

const result = await chain.invoke({ topic: "猫" });
console.log(result);

一句话总结

  • 原生 API 的 invoke:向模型发一次请求,等它回复。
  • LangChain 的 invoke:按下任何组件的"运行"按钮,组件接收输入、执行核心功能、返回结果。
  • 看到 something.invoke(...) 时,就知道它在运行这个东西,并等待结果。

三、@langchain/core/tools

@langchain/core/tools 是 LangChain 里用于定义和管理工具的核心模块。它的作用是把任何 JavaScript/TypeScript 函数封装成标准接口,让大模型、代理或链都能统一调用。

StructuredTool 类是所有工具的基座,它本身就是 Runnable 的子类,意味着工具可以像链中的其他组件一样被调用和组合。

每个工具必须包含三个关键信息:

  • name:唯一标识符
  • description:告诉模型什么情况下用这个工具
  • schema:定义输入参数的结构和类型(通常用 zod)

@langchain/core/tools 提供了 tool 函数,这是目前最推荐的创建方式:

typescript 复制代码
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const multiply = tool(
  ({ a, b }: { a: number; b: number }) => {
    return a * b;
  },
  {
    name: "multiply",
    description: "将两个数字相乘",
    schema: z.object({
      a: z.number().describe("第一个操作数"),
      b: z.number().describe("第二个操作数"),
    }),
  }
);

await multiply.invoke({ a: 3, b: 4 }); // 返回 12

四、tool_calls

理解 tool_calls 的关键在于一个核心转变:大模型从"只会说"变成了"可以动手用工具"

传统的 LLM 只能输出文本。tool_calls 机制让它能输出一个结构化的"指令",比如"我想调用 get_weather 这个工具,参数是 location=Beijing"。

没有 tool_calls 的时候:你问"北京天气",模型只能说"很抱歉,我无法实时查询天气"。

有了 tool_calls:模型会输出类似 { "name": "get_weather", "arguments": { "location": "Beijing" } } 这样的东西。你需要解析这个指令,自己去执行查询,把结果再给模型,模型才能最终回答你。

重点是:tool_calls 不是模型在执行函数,而是模型请求你去执行一个函数。

OpenAI 原生 API 中的 tool_calls

原生 API 返回的 tool_calls 长这样:

typescript 复制代码
const response_message = {
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"location\":\"Beijing\"}"  // JSON 字符串
      }
    }
  ]
}

处理流程:

  1. 检查 response_message 中是否有 tool_calls
  2. 如果有,遍历 tool_calls,根据 function.name 找到本地函数
  3. 解析 arguments,执行函数,得到结果
  4. 把结果构造为一条新消息发给模型(role: "tool", tool_call_id: call_abc123)
  5. 模型收到后,最终生成自然语言回答

LangChain 中的 tool_calls

LangChain 封装了上述过程。底层同样是解析 tool_calls,但上层提供了 bind_toolswith_structured_output 以及 ToolNode(在 LangGraph 中)等高级接口。

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { AIMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const getWeather = tool(
  async ({ location }: { location: string }) => {
    return `${location}的天气是晴天, 25°C`;
  },
  {
    name: "get_weather",
    description: "获取指定位置的天气信息",
    schema: z.object({
      location: z.string().describe("城市名称")
    })
  }
);

const model = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
}).bindTools([getWeather]);

const response: AIMessage = await model.invoke("北京天气?");

console.log(response.tool_calls);
// [
//   {
//     name: 'get_weather',
//     args: { location: 'Beijing' },  // 已经是对象了
//     id: 'call_abc123',
//     type: 'tool_call'
//   }
// ]

if (response.tool_calls && response.tool_calls.length > 0) {
  for (const toolCall of response.tool_calls) {
    console.log(`调用工具: ${toolCall.name}`);
    console.log(`参数:`, toolCall.args);
    console.log(`调用ID: ${toolCall.id}`);
    
    if (toolCall.name === "get_weather") {
      const result = await getWeather.invoke(toolCall.args);
      console.log(`结果: ${result}`);
    }
  }
}

TypeScript 中的类型定义:

typescript 复制代码
interface ToolCall {
  name: string;
  args: Record<string, any>;  // 已解析的对象
  id: string;
  type?: "tool_call";
}

interface AIMessage {
  content: string;
  tool_calls?: ToolCall[];
}

OpenAI 原生和 LangChain 的对比:

特性 OpenAI 原生 LangChain
参数格式 JSON 字符串 已解析的对象
关键字段 id, type, function name, args, id
返回载体 response_message["tool_calls"] ai_message.tool_calls
使用体验 需要手动解析、匹配、执行 提供配套工具和链条

一句话总结

tool_calls 是大模型输出的一批结构化的"函数调用指令",它自己不执行,而是由你的应用程序或框架(如 LangChain)接收、执行,然后将结果回传给模型。

完整的工具调用循环示例:

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { AIMessage, ToolMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const getWeather = tool(
  async ({ location }: { location: string }) => {
    const weatherData = {
      Beijing: "晴天, 25°C",
      Shanghai: "多云, 22°C",
      Guangzhou: "阵雨, 28°C"
    };
    return weatherData[location as keyof typeof weatherData] || "未知地点";
  },
  {
    name: "get_weather",
    description: "获取指定城市的天气",
    schema: z.object({
      location: z.string().describe("城市名称")
    })
  }
);

const model = new ChatOpenAI({
  model: "gpt-3.5-turbo",
  temperature: 0
}).bindTools([getWeather]);

async function chatWithTools(userInput: string) {
  const messages = [new AIMessage(userInput)];
  
  let response = await model.invoke(messages);
  messages.push(response);
  
  while (response.tool_calls && response.tool_calls.length > 0) {
    for (const toolCall of response.tool_calls) {
      const result = await getWeather.invoke(toolCall.args);
      
      const toolMessage = new ToolMessage({
        content: result,
        tool_call_id: toolCall.id
      });
      messages.push(toolMessage);
    }
    
    response = await model.invoke(messages);
    messages.push(response);
  }
  
  return response.content;
}

const finalAnswer = await chatWithTools("北京天气怎么样?");
console.log(finalAnswer);

五、@langchain/core/messages

Messages 是 LangChain 中模型上下文的基本单元,代表与 LLM 交互时的输入和输出,包含内容和元数据。

每个消息对象包含三部分:

  • Role:消息类型(system、user、assistant、tool)
  • Content:实际内容(文本、图片、音频等)
  • Metadata:可选字段,如消息 ID、token 用量等

四种消息类型

类型 用途
SystemMessage 设定模型的行为、角色和回答准则
HumanMessage 用户输入和交互
AIMessage 模型生成的响应,可包含文本、工具调用和元数据
ToolMessage 工具调用的输出结果

SystemMessage

系统消息用于引导模型行为:

typescript 复制代码
import { SystemMessage, HumanMessage } from "langchain";

const systemMsg = new SystemMessage(`
  You are a senior TypeScript developer with expertise in web frameworks.
  Always provide code examples and explain your reasoning.
  Be concise but thorough in your explanations.
`);

const messages = [
  systemMsg,
  new HumanMessage("How do I create a REST API?")
];
const response = await model.invoke(messages);

HumanMessage

最简单的用法是直接传字符串:

typescript 复制代码
const response = await model.invoke("What is machine learning?");

也可以显式构造实例:

typescript 复制代码
const humanMsg = new HumanMessage({
  name: "alice",
  content: "Hello!",
  id: "msg_123",
});

AIMessage

模型调用返回的就是 AIMessage:

typescript 复制代码
const response = await model.invoke("Explain AI");
console.log(typeof response); // AIMessage

可以在对话中插入 AIMessage 来模拟历史记录:

typescript 复制代码
const aiMsg = new AIMessage("I'd be happy to help you with that question!");
const messages = [
  new SystemMessage("You are a helpful assistant"),
  new HumanMessage("Can you help me?"),
  aiMsg,
  new HumanMessage("Great! What's 2+2?")
];
const response = await model.invoke(messages);

当模型需要调用工具时,AIMessage 会带上 tool_calls:

typescript 复制代码
const modelWithTools = model.bindTools([getWeather]);
const response = await modelWithTools.invoke("What's the weather in Paris?");

for (const toolCall of response.tool_calls) {
  console.log(`Tool: ${toolCall.name}`);
  console.log(`Args: ${toolCall.args}`);
  console.log(`ID: ${toolCall.id}`);
}

流式返回时,收到的是 AIMessageChunk,可以用 concat 拼成完整消息:

typescript 复制代码
import { AIMessageChunk } from "langchain";

let finalChunk: AIMessageChunk | undefined;
for (const chunk of chunks) {
  finalChunk = finalChunk ? finalChunk.concat(chunk) : chunk;
}

ToolMessage

ToolMessage 用于把工具执行结果传回给模型:

typescript 复制代码
import { AIMessage, ToolMessage } from "langchain";

const aiMessage = new AIMessage({
  content: [],
  tool_calls: [{
    name: "get_weather",
    args: { location: "San Francisco" },
    id: "call_123"
  }]
});

const toolMessage = new ToolMessage({
  content: "Sunny, 72°F",
  tool_call_id: "call_123"
});

const messages = [
  new HumanMessage("What's the weather in San Francisco?"),
  aiMessage,
  toolMessage
];

const response = await model.invoke(messages);
相关推荐
sunneo2 小时前
S16.1第一性原理做产品(1):从《创新者的窘境》看AI颠覆——为什么大模型不是护城河
人工智能·产品运营·产品经理·ai编程·ai-native
小帅不太帅2 小时前
为了不错过各种 AI 活动,我做了个网站
人工智能·ai编程
红信鸽2 小时前
从 0 到 1:基于 Cursor 与 GPT-5/Claude 4 构建企业级智能重构平台
ai编程
vndmea3 小时前
让 GPT 判断,让国产模型干活:我的 AI 编程降本实践
ai编程
卡卡罗特AI3 小时前
ChatGPT 5.6模型终于发布!然而Codex被夺舍了!
chatgpt·ai编程
leeyi3 小时前
Eino 的“你好世界“背后发生了什么(第48篇-E34)
aigc·agent·ai编程
岁岁养乐多3 小时前
Claude Code 的 Prompt 工程:从静态分离到缓存优化的深度解析
prompt·ai编程
惢雨3 小时前
AI 开发技巧(实战技巧)
人工智能·ai编程
怕浪猫4 小时前
第12章 实战项目四:电商销售与服务Agent
aigc·openai·ai编程