LangChain.js:Tool、Memory 与 Agent 的深度解析与实战

在构建 LLM(大语言模型)应用时,我们的开发路径通常遵循一条清晰的进化曲线:从最简单的"你问我答"文本生成,到让模型输出结构化数据,再到赋予模型操作外部世界(API、数据库)的能力,最后构建能够自主规划、拆解任务的智能体(Agent)。

本文将基于 LangChain.js v1.0+ 的最新标准,通过代码实战,带你深入理解 Function Call、Tool、Memory 和 Agent 的底层机制与最佳实践。

1. 基础:与之对话 (Basic Model)

在上一篇文章中,我们介绍了 LangChain.js 的基础。在这个阶段,模型仅仅是一个"文本生成器"。它处于一个封闭的真空中:不知道当前的时间,无法访问互联网,也没有记忆(除非我们将历史记录传给它)。

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  model: "gpt-4o",
  temperature: 0
});

// 基础调用
const response = await model.invoke("你好,请介绍一下你自己。");
console.log(response.content);

局限性: 它是被动的。如果你问它"今天北京天气怎么样?",它只能根据训练时的截止数据告诉你"我无法获取实时信息"。

2. 桥梁:Function Call (函数调用)

为了打破"真空",OpenAI 等模型厂商引入了 Function Calling (现在的标准术语是 Tool Calling)。

核心概念

很多开发者有一个误区:认为 Function Call 是让 LLM 直接运行代码。

事实并非如此。 它的本质是:LLM 能够根据你的自然语言指令,智能地从你提供的工具列表中选择一个,并生成符合该工具参数要求的 JSON 数据。

真正的执行(调用 API、查询数据库),依然由你的代码在本地完成。

原生流程演示

在使用高级封装之前,我们需要理解底层发生了什么。以下是使用 LangChain.js 手动处理 Tool Call 的完整流程:

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, ToolMessage } from "@langchain/core/messages";

// 1. 定义工具的具体逻辑(查询实时天气)
const getCurrentWeather = async (city: string): Promise<string> => {
  // 模拟调用
  const weatherData = {
    city,
    temperature: 25,
    condition: "晴朗",
    updateTime: new Date().toLocaleString(),
  };
  return JSON.stringify(weatherData); // 工具通常返回字符串
};

// 2. 定义工具描述(JSON Schema,供模型理解)
const weatherToolSchema = {
  type: "function",
  function: {
    name: "getCurrentWeather",
    description: "获取指定城市的实时天气信息",
    parameters: {
      type: "object",
      properties: {
        city: { type: "string", description: "城市名称,例如:北京" },
      },
      required: ["city"],
    },
  },
};

// 3. 初始化并绑定工具
const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
// 注意:使用 bindTools 是更现代的写法
const modelWithTools = model.bindTools([weatherToolSchema]);

// 4. 第一轮交互:用户提问
const userQuery = "北京现在的天气怎么样?";
const messages = [new HumanMessage(userQuery)];

const aiResponse = await modelWithTools.invoke(messages);

// 5. 检查模型是否想要调用工具
if (aiResponse.tool_calls && aiResponse.tool_calls.length > 0) {
  console.log("模型决定调用工具:", aiResponse.tool_calls);
  
  const toolCall = aiResponse.tool_calls[0];
  
  // 6. 执行对应的函数 (手动路由)
  if (toolCall.name === "getCurrentWeather") {
    const args = toolCall.args; // LangChain 自动帮我们 parse 好了 JSON
    const toolResult = await getCurrentWeather(args.city);
    
    // 7. 将工具结果封装为 ToolMessage
    const toolMessage = new ToolMessage({
      tool_call_id: toolCall.id!, // 必须回传 ID 以便模型匹配
      content: toolResult,
      name: toolCall.name
    });
    
    // 8. 将工具结果回传给模型,进行第二轮推理
    const finalResponse = await modelWithTools.invoke([
      ...messages, 
      aiResponse, // 必须包含模型上一轮的回复(其中包含 tool_calls 请求)
      toolMessage // 以及工具的执行结果
    ]);
    
    console.log("最终回答:", finalResponse.content);
  }
}

痛点: 你会发现,上面的代码中包含了大量的 if/else 逻辑来判断模型是否调用了工具、调用了哪个工具、以及手动执行并回传结果。如果工具有几十个,这部分代码将难以维护。

3. 封装:LangChain.js 中的 Tool

为了解决上述痛点,LangChain.js 提供了 Tool 的标准定义和 @langchain/core/tools 中的 tool 辅助函数。

它将函数逻辑参数Schema (Zod)元数据 封装在一起,让工具具备更强的可复用性。

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

// 使用 tool 函数封装
const calculator = tool(
  async ({ operation, a, b }) => {
    console.log(`正在执行计算: ${a} ${operation} ${b}`);
    switch (operation) {
      case "add": return `${a + b}`;
      case "multiply": return `${a * b}`;
      default: return "Error: Unknown operation";
    }
  },
  {
    name: "calculator",
    description: "执行基本的数学运算(加法或乘法)",
    // Zod 不仅用于校验,LangChain 还会自动将其转换为 OpenAI 需要的 JSON Schema
    schema: z.object({
      operation: z.enum(["add", "multiply"]).describe("运算类型"),
      a: z.number().describe("第一个数字"),
      b: z.number().describe("第二个数字"),
    }),
  }
);

// 现在的调用变得非常简单
const llmWithCalc = model.bindTools([calculator]);
const res = await llmWithCalc.invoke("计算 5 乘以 8 是多少");

// res.tool_calls 会自动包含解析好的参数
console.log("工具调用结果:", res.tool_calls);

Memory:构建对话上下文

默认情况下,LLM API 是无状态的(Stateless)。要实现连续对话,我们需要管理对话历史记录(History)。

在 LangChain.js v1.0+ 中,我们使用 MemorySaver 和其他检查点(Checkpointer)来管理对话状态。

1. 本地内存存储(MemorySaver)
typescript 复制代码
import { MemorySaver } from "@langchain/langgraph";
import { createAgent } from "@langchain/langgraph";

// 初始化内存检查点
const memorySaver = new MemorySaver();

// 创建智能代理
const agent = createAgent({
  model: "gpt-4o", // 使用最新模型
  tools: [], // 工具列表
  checkpointer: memorySaver,
});

// 开始对话
const result = await agent.invoke(
  { messages: [{ role: "user", content: "你好,我是Bob。" }] },
  { configurable: { thread_id: "1" } }
);

console.log("第一次回复:", result.messages[0].content);

// 继续对话
const nextResult = await agent.invoke(
  { messages: [{ role: "user", content: "你记得我叫什么吗?" }] },
  { configurable: { thread_id: "1" } }
);

console.log("第二次回复:", nextResult.messages[0].content);
2. Redis 持久化存储

对于需要在多个服务实例间共享对话状态的场景,可以使用 Redis 作为持久化存储:

typescript 复制代码
import { RedisCheckpointer } from "@langchain/langgraph-checkpoint-redis";
import { createAgent } from "@langchain/langgraph";

// 配置 Redis 连接
const redisUrl = "redis://localhost:6379";
const redisCheckpointer = new RedisCheckpointer(redisUrl);

// 创建智能代理
const agent = createAgent({
  model: "gpt-4o",
  tools: [],
  checkpointer: redisCheckpointer,
});

// 使用与之前相同的代码调用
3. PostgreSQL 持久化存储

对于需要结构化存储和更强大查询能力的场景,可以使用 PostgreSQL:

typescript 复制代码
import { PostgresCheckpointer } from "@langchain/langgraph-checkpoint-postgres";
import { createAgent } from "@langchain/langgraph";

// 配置 PostgreSQL 连接
const dbUrl = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const postgresCheckpointer = new PostgresCheckpointer({
  connectionString: dbUrl,
  tableName: "langgraph_checkpoints",
});

// 创建智能代理
const agent = createAgent({
  model: "gpt-4o",
  tools: [],
  checkpointer: postgresCheckpointer,
});

// 使用与之前相同的代码调用
4. 对话历史压缩

为了确保对话历史不超过模型的上下文限制,我们可以使用 ConversationBufferWindowMemory 或自定义压缩策略:

typescript 复制代码
import { ConversationBufferWindowMemory } from "@langchain/memory";
import { ChatOpenAI } from "@langchain/openai";

// 创建带窗口记忆的模型
const model = new ChatOpenAI({
  model: "gpt-4o",
  temperature: 0,
});

const memory = new ConversationBufferWindowMemory({
  k: 5, // 保留最近5轮对话
  memoryKey: "chat_history",
  returnMessages: true,
});

// 在每次调用前,将历史记录注入到消息中
const history = await memory.loadMemoryVariables({});
const messages = [
  new HumanMessage("你好,我是Bob。"),
  ...history.chat_history,
];

const response = await model.invoke(messages);
await memory.saveContext({ input: "你好,我是Bob。" }, { output: response.content });

长期记忆:知识库增强

Checkpointer 解决的是"短期/会话级记忆"。如果需要让 Agent 记住几天前甚至几个月前的信息,我们需要引入 Vector Store (向量数据库)。

  • 原理:将用户的重要信息(如个人喜好、历史决策)Embed 之后存入向量库(如 Pinecone, Weaviate)。
  • 检索:在每次对话前,先去向量库搜索相关的"记忆片段",将其作为 Context 注入到 Prompt 中。这是 RAG (Retrieval-Augmented Generation) 的一种应用形式。
typescript 复制代码
import { MemorySaver } from "@langchain/langgraph";
import { createAgent } from "@langchain/langgraph";
import { OpenAIEmbeddings } from "@langchain/openai";
import { Chroma } from "@langchain/community/vectorstores/chroma";

// 初始化向量存储
const embeddings = new OpenAIEmbeddings();
const vectorStore = new Chroma(
  embeddings,
  { collectionName: "long_term_memory" }
);

// 创建智能代理
const agent = createAgent({
  model: "gpt-4o",
  tools: [],
  checkpointer: new MemorySaver(),
});

// 保存长期记忆
const saveLongTermMemory = async (userId: string, content: string) => {
  await vectorStore.addDocuments([
    {
      pageContent: content,
      metadata: { userId, timestamp: new Date().toISOString() }
    }
  ]);
};

// 检索长期记忆
const retrieveLongTermMemory = async (userId: string, query: string) => {
  const results = await vectorStore.similaritySearch(query, 3);
  return results.map(r => r.pageContent);
};

// 在代理中使用
const result = await agent.invoke(
  { messages: [{ role: "user", content: "告诉我你记得我的名字。" }] },
  { configurable: { thread_id: "1" } }
);

// 保存新记忆
await saveLongTermMemory("1", result.messages[0].content);

// 之后可以检索
const memories = await retrieveLongTermMemory("1", "名字");
console.log("长期记忆:", memories);

4. 编排:LangChain.js 中的 Agent

当我们有了 Model 和封装好的 Tool,谁来负责那个繁琐的"循环"?

如果用户问:"先查一下北京的天气,然后根据气温计算一下如果要穿三层衣服,每层衣服的平均厚度"。

这需要一个系统能够:

  1. 思考 (Thought):先调天气工具。
  2. 行动 (Action):执行天气工具。
  3. 观察 (Observation):拿到 25度。
  4. 再思考:现在需要计算。
  5. 再行动:执行计算工具。
  6. 最终回答

现代 Agent 构建指南

在现代 LangChain.js 中,我们推荐使用 createAgent,这是最稳定且通用的 Agent 类型。

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

// 搜索工具 - 用于检索指定关键词的信息
const search = tool(
  ({ query }) => `搜索结果:${query}`,
  {
    name: "search",
    description: "搜索信息",
    schema: z.object({
      query: z.string().describe("要搜索的关键词"),
    }),
  }
);

// 天气查询工具 - 获取指定地点的天气信息
const getWeather = tool(
  ({ location }) => `${location} 的天气:晴天,72华氏度`,
  {
    name: "get_weather",
    description: "获取指定地点的天气信息",
    schema: z.object({
      location: z.string().describe("要查询天气的地点"),
    }),
  }
);

// 创建智能代理
const agent = createAgent({
  model: "gpt-4o",
  tools: [search, getWeather],
});

// 调用代理查询北京天气
const result = await agent.invoke({
  messages: [new HumanMessage("北京的天气怎么样?")]
});

console.log("最终回答:", result.messages[0].content);

高级 Agent 使用技巧

1. 添加自定义提示词
typescript 复制代码
const agent = createAgent({
  model: "gpt-4o",
  tools: [search, getWeather],
  // 自定义系统提示词
  systemMessage: "你是一个天气助手,专注于提供准确的天气信息和建议。",
});
2. 处理工具调用错误
typescript 复制代码
const agent = createAgent({
  model: "gpt-4o",
  tools: [search, getWeather],
  // 自定义错误处理
  onError: (error) => {
    console.error("工具调用错误:", error);
    return `抱歉,我遇到了问题: ${error.message}`;
  },
});
3. 限制工具调用次数
typescript 复制代码
const agent = createAgent({
  model: "gpt-4o",
  tools: [search, getWeather],
  // 限制工具调用次数
  maxIterations: 5,
});
4. 与外部系统集成
typescript 复制代码
// 示例:使用实际API获取天气
const getRealWeather = tool(
  async ({ location }) => {
    const response = await fetch(`https://api.weatherapi.com/v1/current.json?key=YOUR_KEY&q=${location}`);
    const data = await response.json();
    return JSON.stringify({
      location: data.location.name,
      tempC: data.current.temp_c,
      condition: data.current.condition.text
    });
  },
  {
    name: "get_real_weather",
    description: "获取指定地点的实时天气信息",
    schema: z.object({
      location: z.string().describe("要查询天气的地点"),
    }),
  }
);

5. 实战:构建多步骤智能助手

让我们构建一个完整的多步骤助手,结合天气查询和计算功能:

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

// 1. 定义工具
const getWeather = tool(
  async ({ location }) => {
    // 模拟实际API调用
    const temperature = Math.floor(Math.random() * 30) + 15;
    return JSON.stringify({
      location,
      temperature,
      condition: temperature > 25 ? "晴朗" : "多云"
    });
  },
  {
    name: "get_weather",
    description: "获取指定地点的实时天气信息",
    schema: z.object({
      location: z.string().describe("要查询天气的地点"),
    }),
  }
);

const calculator = tool(
  async ({ operation, a, b }) => {
    switch (operation) {
      case "add": return `${a + b}`;
      case "multiply": return `${a * b}`;
      default: return "Error: Unknown operation";
    }
  },
  {
    name: "calculator",
    description: "执行基本的数学运算(加法或乘法)",
    schema: z.object({
      operation: z.enum(["add", "multiply"]).describe("运算类型"),
      a: z.number().describe("第一个数字"),
      b: z.number().describe("第二个数字"),
    }),
  }
);

// 2. 创建Agent
const memorySaver = new MemorySaver();
const agent = createAgent({
  model: new ChatOpenAI({ model: "gpt-4o", temperature: 0 }),
  tools: [getWeather, calculator],
  checkpointer: memorySaver,
});

// 3. 运行多步骤对话
const result = await agent.invoke(
  { 
    messages: [new HumanMessage("北京今天的天气是25度,如果要穿三层衣服,每层衣服的平均厚度是1.5厘米,计算总厚度。")] 
  },
  { configurable: { thread_id: "weather_calculation" } }
);

console.log("最终回答:", result.messages[0].content);

总结

  • Model: 大脑,负责推理,但无法直接行动。
  • Function Call: 协议,让 Model 输出结构化指令。
  • Tool: 封装,将代码逻辑包装成 Model 可理解的积木。
  • Agent: 编排者,利用 Model 的推理能力,循环调用 Tool,解决复杂的多步问题。
相关推荐
吴佳浩 Alben2 小时前
Python入门指南(六) - 搭建你的第一个YOLO检测API
开发语言·python·yolo
七夜zippoe2 小时前
使用OpenLLM管理轻量级大模型服务
架构·langchain·大模型·kv·轻量
love530love2 小时前
Win11+RTX3090 亲测 · ComfyUI Hunyuan3D 全程实录 ③:diso 源码编译实战(CUDA 13.1 零降级)
开发语言·人工智能·windows·python·comfyui·hunyuan3d·diso
qq_377112372 小时前
JAVA的平凡之路——此峰乃是最高峰JVM-GC垃圾回收器(2)-06
java·开发语言·jvm
霁月的小屋2 小时前
Vue响应式数据全解析:从Vue2到Vue3,ref与reactive的实战指南
前端·javascript·vue.js
weixin_468635292 小时前
用python获取双色球历史数据,纯数据处理,非爬虫
开发语言·爬虫·python
李少兄2 小时前
深入理解 Java Web 开发中的 HttpServletRequest 与 HttpServletResponse
java·开发语言·前端
kylezhao20192 小时前
C#变量 + 工业常用数据类型:重点 byte/int/float
开发语言·c#·c#上位机
yyy(十一月限定版)2 小时前
c语言——二叉树
c语言·开发语言·数据结构