聊天机器人
构建具备记忆、工具调用和流式响应能力的智能聊天机器人
📚 学习目标
学完这篇文章后,你将能够:
- 使用
MemorySaver实现多轮对话记忆 - 构建具备工具调用能力的智能助手
- 实现流式响应以优化用户体验
前置知识
在开始学习之前,建议先阅读:
你需要了解:
MessagesAnnotation的基本用法
1 基础聊天机器人
最简单的聊天机器人只需要一个节点:接收消息,调用 LLM,返回回复。
import { StateGraph, START, END } from '@langchain/langgraph';
import { MessagesAnnotation } from '@langchain/langgraph';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage } from '@langchain/core/messages';
const model = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0.2 });
// 最小聊天节点:把 messages 交给模型,返回新消息
const chatbotNode = async (state: typeof MessagesAnnotation.State) => {
const response = await model.invoke(state.messages);
return { messages: [response] };
};
export const chatApp = new StateGraph(MessagesAnnotation)
.addNode('chatbot', chatbotNode)
.addEdge(START, 'chatbot')
.addEdge('chatbot', END)
.compile();
// 调用示例
await chatApp.invoke({ messages: [new HumanMessage('你好,介绍一下你自己')] });代码解析:
MessagesAnnotation提供了 messages 的合并规则(把新消息追加到历史里)。- 节点只返回“增量更新”:
{ messages: [response] }。 - 这是一条最短链路:用户 -> 模型 -> 输出。
2 从 Demo 到“能用”:你需要补齐哪些能力?
一个产品级聊天机器人至少会遇到这些问题:
| 能力 | 为什么需要 | 你会用到的章节 |
|---|---|---|
| 记忆(thread_id) | 多轮对话不能每次从头来 | 持久化 |
| 工具调用 | 查天气/查资料/查数据库 | 工具调用 |
| 流式输出 | 打字机效果 + 降低感知延迟 | 流式处理 |
| 错误恢复 | 工具失败、超时、重试 | 错误处理 |
3 添加记忆 (Memory)
为了让机器人记住之前的对话,我们需要:
- 使用
MemorySaver作为 checkpointer。 - 在调用时传入
thread_id。
import { MemorySaver } from '@langchain/langgraph';
import { HumanMessage } from '@langchain/core/messages';
const checkpointer = new MemorySaver();
// 👇 编译时传入 checkpointer
const app = workflow.compile({ checkpointer });
// 👇 调用时传入 thread_id
const config = { configurable: { thread_id: "user-123" } };
await app.invoke({ messages: [new HumanMessage("Hi!")] }, config);
// 同一 thread_id 再次调用:历史会自动加载并合并
await app.invoke({ messages: [new HumanMessage('我刚才说了什么?')] }, config);📝 提醒
thread_id 是“会话主键”。建议你在真实产品里用更稳定的格式,例如 userId:conversationId。
4 智能助手 (工具调用)
现代聊天机器人通常需要调用工具(如搜索、查天气)。
架构图
关键代码
利用 bindTools 和 ToolNode 可以快速实现。
下面是一段“可复制”的最小 ReAct Loop(省略真实 API 调用,重点看结构):
import { StateGraph, START, END } from '@langchain/langgraph';
import { MessagesAnnotation } from '@langchain/langgraph';
import { ToolNode } from '@langchain/langgraph/prebuilt';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
const weatherTool = tool(
async ({ city }) => `Weather in ${city}: Sunny`,
{
name: 'get_weather',
description: 'Get current weather for a city',
schema: z.object({ city: z.string() }),
}
);
const tools = [weatherTool];
const modelWithTools = model.bindTools(tools);
const toolNode = new ToolNode(tools);
const agentNode = async (state: typeof MessagesAnnotation.State) => {
const response = await modelWithTools.invoke(state.messages);
return { messages: [response] };
};
const shouldContinue = (state: typeof MessagesAnnotation.State) => {
const lastMessage = state.messages[state.messages.length - 1];
// 简化写法:只要最后一条消息包含 tool_calls,就继续走 tools
if ((lastMessage as { tool_calls?: unknown[] })?.tool_calls?.length) return 'tools';
return END;
};
export const reactChat = new StateGraph(MessagesAnnotation)
.addNode('agent', agentNode)
.addNode('tools', toolNode)
.addEdge(START, 'agent')
.addConditionalEdges('agent', shouldContinue)
.addEdge('tools', 'agent')
.compile();💡 提示
循环边存在时,调用时加 recursionLimit(例如 10-20),避免模型陷入坏循环。
5 流式响应 (Streaming)
LangGraph 支持流式输出,这对于聊天体验至关重要。
import { HumanMessage } from '@langchain/core/messages';
const stream = await reactChat.stream(
{ messages: [new HumanMessage('用 200 字讲一个故事')] },
{ streamMode: 'messages' }
);
for await (const [msg] of stream) {
// msg 通常是 chunk 类型(用于实现打字机效果)
process.stdout.write(String(msg.content));
}当你需要“更底层的事件”(例如工具开始/结束、模型 token 流)时,使用 streamEvents(见 流式处理)。
6 对话风格与系统提示
如果你希望机器人保持统一口吻(例如“专业顾问”或“冷静客服”),可以在消息中添加系统提示:
import { SystemMessage } from '@langchain/core/messages';
const systemPrompt = new SystemMessage(
'你是专业客服,回答必须简洁、礼貌,并在最后给出下一步建议。'
);
await chatApp.invoke({
messages: [systemPrompt, new HumanMessage('我想退货怎么办?')],
});💡 说明
系统提示越清晰,回复风格越稳定。避免过长的系统提示,以免挤占上下文窗口。
7 工具失败与降级
当工具调用失败时,建议做降级处理,避免对话中断:
const safeToolNode = async (state: typeof MessagesAnnotation.State) => {
try {
return await toolNode.invoke(state);
} catch (error) {
return {
messages: [
{
role: 'tool',
content: `工具失败:${(error as Error).message}`,
},
],
};
}
};⚠️ 注意
建议将“工具失败”反馈给 LLM,让它尝试替代方案或向用户解释限制。
8 控制上下文长度
长期对话会导致上下文过长,可以使用“摘要”或“截断策略”:
const summarizeHistory = async (messages: BaseMessage[]) => {
const summary = await model.invoke([
new SystemMessage('请将以下对话总结为 5 条要点'),
...messages,
]);
return String(summary.content);
};ℹ️ 说明
将摘要写回状态后,可以在下一轮对话中替代冗长历史。
9 会话管理与多用户
真实应用中,通常需要维护多个并发会话:
const makeThreadId = (userId: string, conversationId: string) =>
`${userId}:${conversationId}`;
const config = {
configurable: {
thread_id: makeThreadId('user-42', 'conv-001'),
},
};
await app.invoke({ messages: [new HumanMessage('你好')] }, config);💡 建议
把 thread_id 持久化到数据库,避免前端刷新导致会话丢失。
10 简单的前端集成思路
如果你在前端做聊天 UI,可以把“发送/接收”拆成两层:
- 前端只负责维护消息列表
- 后端负责调用 LangGraph,并返回增量消息
// 前端伪代码
const onSend = async (text: string) => {
appendMessage({ role: 'user', content: text });
const resp = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ text }) });
const data = await resp.json();
appendMessage({ role: 'assistant', content: data.reply });
};ℹ️ 说明
流式输出时可用 SSE/WebSocket,把 token 逐步追加到 UI。
11 安全边界与输入校验
聊天机器人通常需要做输入校验与工具白名单控制:
- 限制工具调用范围:只注册必要工具
- 过滤敏感输入:避免提示注入
- 日志审计:记录关键决策路径
const safeTools = [weatherTool, searchTool];
const modelWithTools = model.bindTools(safeTools);⚠️ 注意
不要把“危险操作”暴露给模型(如文件删除、支付等)。
💡 练习题
-
实战题:构建一个“算命大师”聊天机器人。它应该:
- 记住用户的名字和生日(使用 Memory)。
- 有一个
calculate_fortune工具,根据生日计算运势。 - 语气始终神秘兮兮的。
点击查看答案
使用
MemorySaver+thread_id维护会话,工具调用逻辑可复用第 4 节中的 ReAct Loop。 -
操作题:为聊天机器人添加统一风格(系统提示)。
点击查看答案
在每次调用时加入
SystemMessage作为第一条消息。 -
思考题:工具调用失败时,应该如何向用户解释?
点击查看答案
可以提示“工具暂不可用”,并给出替代方案或让用户换个问题。
-
操作题:实现一个“摘要节点”,把历史对话压缩为 5 条要点。
点击查看答案
复用模型生成摘要,然后将摘要写回状态替代历史 messages。
-
思考题:流式输出比普通输出体验好在哪里?
点击查看答案
能显著降低用户等待焦虑,提升响应的“即时感”。
✅ 总结
本章要点:
MemorySaver+thread_id是实现记忆的关键。- 工具调用让聊天机器人变成了真正的“助手”。
- 流式输出能显著提升长文本生成的体验。
下一步:如何让机器人基于文档回答问题?学习RAG 系统。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。