创建和配置文件夹

创建文件夹

1
mkdir langgraph-agent

安装对应包:

  • @langchain/langgraph用于组装代理,拼成图(如名:graph)
  • @langchain/openai使得agent能够调用OpenAI的大语言模型(LLM
  • @langchain/community社区安装包,包含Tavily集成,为agent提供搜索功能

LangGraphLangChain都是组装agent的编排框架,用于构建、管理和部署长时间运行、有状态的agent。

1
npm install @langchain/core@^1.1.33 @langchain/langgraph @langchain/openai @langchain/community@^1.1.24

LangChain

LangChain的promt模板

img


  1. 下载dotenv插件实现环境变量管理。创建.env文件,放入千问密钥
1
2
3
OPENAI_API_KEY="sk-xxxx"
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-coder-turbo
  1. 创建agent问答代码:

其中invoke函数来自Runnable接口,LangChain的doc有说明接口。除了创建的模型对象由该函数调用,Prompt模板也有(可以规定对话主题的模板功能)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import dotenv from 'dotenv';
import { ChatOpenAI } from '@langchain/openai';

dotenv.config();

const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME || "qwen-coder-turbo",
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});

const response = await model.invoke("你的知识库截至什么什么");
console.log(response.content);
  1. 可以看到千问模型还是非常老旧的知识库:

image-20260320235747073


  1. 创建工具函数

tool:用于在 LangChain 中创建工具的函数。第一个参数传入实现的工具,第二个是参数列表,包含工具名字、工具描述、校验。

bindTools:绑定一个tool数组,将工具列表绑在模型链上,后面用llmWithTools.invoke()时不用每次再传一边tools

SystemMessages:元指令,作为指导输入

HumanMessages:用户输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import dotenv from 'dotenv';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import fs from 'node:fs/promises';
import {
SystemMessage,
HumanMessage,
ToolMessage,
type BaseMessage,
} from '@langchain/core/messages';

dotenv.config();

const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME || "qwen-coder-turbo",
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});

const readFileTool = tool(
async ({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
console.log(`  [工具调用] read_file("${filePath}") - 成功读取 ${content.length} 字节`);
return `文件内容:\n${content}`;
},
{
name: 'tool_read_file',
description: '用此工具来读取文件内容。当用户要求读取文件、查看代码、分析文件内容时,调用此工具。输入文件路径(可以是相对路径或绝对路径)。',
schema: z.object({
filePath: z.string().describe('要读取的文件路径'),
}),
}
);

const tools = [
readFileTool
];

const modelWithTools = model.bindTools(tools);

const messages: BaseMessage[] = [
new SystemMessage(`你是一个代码助手,可以使用工具读取文件并解释代码。

工作流程:
1. 用户要求读取文件时,立即调用 read_file 工具
2. 等待工具返回文件内容
3. 基于文件内容进行分析和解释

可用工具:
- read_file: 读取文件内容(使用此工具来获取文件内容)
`),
new HumanMessage('请读取 src/tool-file-read.mts 文件内容并解释代码')
];


let response = await modelWithTools.invoke(messages);
// console.log(response);

messages.push(response);

while (response.tool_calls && response.tool_calls.length > 0) {

console.log(`\n[检测到 ${response.tool_calls.length} 个工具调用]`);

// 执行所有工具调用
const toolResults = await Promise.all(
response.tool_calls.map(async (tc) => {
const structuredTool = tools.find((t) => t.name === tc.name);
if (!structuredTool) {
return `错误: 找不到工具 ${tc.name}`;
}

console.log(`  [执行工具] ${tc.name}(${JSON.stringify(tc.args)})`);
try {
const result = await structuredTool.invoke(tc);
return typeof result === "string" ? result : String(result);
} catch (error) {
const err = error as Error;
return `错误: ${err.message}`;
}
})
);

// 将工具结果添加到消息历史
response.tool_calls.forEach((toolCall, index) => {
if (toolResults[index] == null) {
throw new Error(`工具结果为空,无法构造 ToolMessage: ${toolCall.name}`);
}
if (toolCall.id == null) {
throw new Error(`工具调用缺少 id,无法构造 ToolMessage: ${toolCall.name}`);
}
messages.push(
new ToolMessage({
content: toolResults[index],
tool_call_id: toolCall.id,
})
);
});

// 再次调用模型,传入工具结果
console.log(messages);
response = await modelWithTools.invoke(messages);
}

console.log('\n[最终回复]');
console.log(response.content);

通过学习可以知道,Langchain知识让我们能够调用LLM提取出用户输入问题参数,返回一个HumanMessage对象,如果用到了tool函数,那么通过创建自定义函数,与LLM模型链绑定,则可以得到tool_calls。通过异步调用tool_calls并将结果提取出contenttool_call_id,将其转换成ToolMessage(可以看到以下代码是toolResults的结果),重新push进messages数组,通过大模型分析是否得到用户想要的结果,并把结果输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
ToolMessage {
"content": "文件内容:\nimport dotenv from 'dotenv';\r\nimport { ChatOpenAI } from '@langchain/openai';\r\n
import { tool } from '@langchain/core/tools';\r\nimport { z } from 'zod';\r\nimport fs from 'node:fs/promise
s';\r\nimport {\r\n SystemMessage,\r\n HumanMessage,\r\n ToolMessage,\r\n type BaseMessage,\r\n} fro
m '@langchain/core/messages';\r\n\r\ndotenv.config();\r\n\r\nconst model = new ChatOpenAI({\r\n modelName:
process.env.MODEL_NAME || \"qwen-coder-turbo\",\r\n apiKey: process.env.OPENAI_API_KEY,\r\n configurati
on: {\r\n baseURL: process.env.OPENAI_BASE_URL,\r\n },\r\n});\r\n\r\nconst readFileTool = tool(\r\n
async ({ filePath }) => {\r\n const content = await fs.readFile(filePath, 'utf-8');\r\n console.l
og(` [工具调用] read_file(\"${filePath}\") - 成功读取 ${content.length} 字节`);\r\n return `文件内容:\
\n${content}`;\r\n },\r\n {\r\n name: 'tool_read_file',\r\n description: '用此工具来读取文件内
容。当用户要求读取文件、查看代码、分析文件内容时,调用此工具。输入文件路径(可以是相对路径或绝对路径)。',\r
\n schema: z.object({\r\n filePath: z.string().describe('要读取的文件路径'),\r\n }),\r\n
}\r\n);\r\n\r\nconst tools = [\r\n readFileTool\r\n];\r\n\r\nconst modelWithTools = model.bindTools(tools
);\r\n\r\nconst messages: BaseMessage[] = [\r\n new SystemMessage(`你是一个代码助手,可以使用工具读取文件
并解释代码。\r\n\r\n工作流程:\r\n1. 用户要求读取文件时,立即调用 read_file 工具\r\n2. 等待工具返回文件内容\
r\n3. 基于文件内容进行分析和解释\r\n\r\n可用工具:\r\n- read_file: 读取文件内容(使用此工具来获取文件内容)\
r\n`),\r\n new HumanMessage('请读取 src/tool-file-read.mts 文件内容并解释代码')\r\n];\r\n\r\n\r\nlet respo
nse = await modelWithTools.invoke(messages);\r\n// console.log(response);\r\n\r\nmessages.push(response);\r\
n\r\nwhile (response.tool_calls && response.tool_calls.length > 0) {\r\n\r\n console.log(`\\n[检测到 ${res
ponse.tool_calls.length} 个工具调用]`);\r\n\r\n // 执行所有工具调用\r\n const toolResults = await Promis
e.all(\r\n response.tool_calls.map(async (tc) => {\r\n const structuredTool = tools.find((t) =>
t.name === tc.name);\r\n if (!structuredTool) {\r\n return `错误: 找不到工具 ${tc.name}`
;\r\n }\r\n\r\n console.log(` [执行工具] ${tc.name}(${JSON.stringify(tc.args)})`);\r\n
try {\r\n const result = await structuredTool.invoke(tc);\r\n console.log(result);
\r\n return typeof result === \"string\" ? result : String(result);\r\n } catch (error) {
\r\n const err = error as Error;\r\n return `错误: ${err.message}`;\r\n }\r\n
})\r\n );\r\n\r\n // console.log(toolResults);\r\n\r\n // 将工具结果添加到消息历史\r\n response
.tool_calls.forEach((toolCall, index) => {\r\n if (toolResults[index] == null) {\r\n throw new
Error(`工具结果为空,无法构造 ToolMessage: ${toolCall.name}`);\r\n }\r\n if (toolCall.id == null)
{\r\n throw new Error(`工具调用缺少 id,无法构造 ToolMessage: ${toolCall.name}`);\r\n }\r\n
messages.push(\r\n new ToolMessage({\r\n content: toolResults[index],\r\n to
ol_call_id: toolCall.id,\r\n })\r\n );\r\n });\r\n\r\n // 再次调用模型,传入工具结果\r\n
console.log(messages);\r\n response = await modelWithTools.invoke(messages);\r\n}\r\n\r\nconsole.log('\\n[
最终回复]');\r\n// console.log(response);\r\n// console.log(response.content);",
"name": "tool_read_file",
"additional_kwargs": {},
"response_metadata": {},
"tool_call_id": "call_cd44802ac14e45729fd4bc"
}