[LangGrpah] Tool calls demo
Overview
A small demo, using LangGrpah to calling real tool to write an artical about the topic user defined.
Project structure
tool-calls
├── output
│ ├── 当 TypeScript 遇见 Go:一次关于“换芯”的工程化重塑之旅.md
│ └── 当 TypeScript 遇见 Go:一次关于“换芯”的工程化重塑之旅.png
├── package.json
├── pnpm-lock.yaml
└── src
├── graph
│ ├── agent.ts
│ ├── content_node.ts
│ ├── image_node.ts
│ ├── summary_node.ts
│ └── title_node.ts
├── index.ts
├── model.ts
├── prompt.ts
├── state.ts
└── tools
├── index.ts
└── search.ts
grahp: define all the nodesl, with an exported agent
tools: define all the tools
Code
state.ts
import { z } from "zod/v4";
// 整张图状态的Schema
export const Schema = z.object({
topic: z.string().describe("文章主题"),
title: z.string().describe("文章标题"),
content: z.string().describe("文章内容"),
summary: z.string().describe("文章摘要"),
image_path: z.string().describe("图片路径"),
});
// 基于Schema生成的ts类型
export type TArticle = z.infer<typeof Schema>;
model.ts
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
export function getChatGPT(): ChatOpenAI {
return new ChatOpenAI({
model: "gpt-5.1",
temperature: 0.7,
});
}
prompt.ts:
// 系统提示词
export const SYSTEM_PROMPT = `
你是一位资深的内容编辑,擅长各种题材的写作。
你正在撰写一篇主题为 {topic} 的文章,字数在6000个汉字左右,你力求做到结构严谨、行文优美、可读性强。
`;
// 标题提示词
export const TITLE_PROMPT = `
结合指定的主题,给文章起一个贴切的标题。
特别注意:只生成一个最合适的标题即可,以纯文本格式返回,无需生成其他额外信息。
`;
// 内容提示词
export const CONTENT_PROMPT = `
文章的标题为:《{title}》
请先根据文章主题,在互联网上检索相关资料。
接下来,根据资料的检索结果,按照总-分-总的结构,生成文章的主要段落。段落数在3~6段之间,可以结合你的经验和主题灵活调整。
下面是一个可供参考的段落结构:
1. xxx综述
2. xxx的历史演进
3. xxx的核心概念
4. xxx实战
5. xxx总结
格式要求:生成的内容纯文本格式,每一段都要有一个二级标题(##)。段落之间保留2行空行。
`;
// 文章摘要
export const SUMMARY_PROMPT = `
现在,文章已经写好了,请你根据文章的主题、标题和正文内容,生成一段30~60个汉字的摘要,总结这篇文章的核心内容。
标题:{title}
正文内容:{content}
`;
// 文章图片
export const IMAGE_PROMPT = `
你是一位资深的插画师,擅长为各种文章创作精美的插图。
现在有一篇文章,具体信息如下:
主题:{topic}
标题:{title}
摘要:{summary}
请你为该文章创造一副插画,展示文章所要传达的核心内容。要求画质精美、符合逻辑、表达力强,且意境深远,给人无尽回味。
特别注意:如果文章中包含文字,需要确保文字表达通顺。
`;
index.ts
import "dotenv/config";
import { buildGraph, writeArticle, dumpMarkdown } from "./graph/agent.ts";
// 入口文件
async function main() {
// 1. 构造agent
const agent = buildGraph();
// 2. 写文章:agent、主题
const finalState = await writeArticle(agent, "typescript用go语言换芯");
// 3. 将文章导出为markdown格式
dumpMarkdown(finalState);
}
main();
tools/search.ts
// 搜索工具
import { z } from "zod";
import { tool } from "@langchain/core/tools";
// 工具方法参数 schema
const schema = z.object({
query: z.string().describe("搜索的关键字"),
});
export type searchInput = z.infer<typeof schema>;
const func = async ({ query }: searchInput): Promise<string> => {
console.log(`正在调用工具 [search] 进行搜索,搜索的关键词为:${query}`);
// SerpAPI
const baseUrl = "https://serpapi.com/search.json";
const apiKey = process.env.SERPER_API_KEY;
if (!apiKey) throw new Error("缺少Serp Api Key");
// 构建url的查询参数
const params = new URLSearchParams({
engine: "google",
q: query,
api_key: apiKey,
gl: "cn",
hl: "zh-cn",
});
try {
const response = await fetch(`${baseUrl}?${params.toString()}`);
if (!response.ok) throw new Error(`搜索失败,状态码为${response.status}`);
const json = await response.json();
if (json.organic_results && json.organic_results.length > 0) {
// 说明这一次搜索是有效的
return json.organic_results[0].snippet;
}
return "没有搜索到相关结果";
} catch (err) {
console.error("Search工具出现错误,错误信息为:", err);
return "搜索错误";
}
};
const search = tool(func, {
name: "search",
description: "根据关键词,在互联网上检索相关的信息",
schema,
});
export default search;
graph/title_node.ts
import type { TArticle } from "../state.ts";
import { SYSTEM_PROMPT, TITLE_PROMPT } from "../prompt.ts";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { getChatGPT } from "../model.ts";
import { StringOutputParser } from "@langchain/core/output_parsers";
export async function titleNode(state: TArticle): Promise<Partial<TArticle>> {
if (!state.topic) {
throw new Error("主题不能为空");
}
const topic = state.topic;
const pt = ChatPromptTemplate.fromMessages([
["system", SYSTEM_PROMPT],
["human", TITLE_PROMPT],
]);
const model = getChatGPT();
const chain = pt.pipe(model).pipe(new StringOutputParser());
const title = await chain.invoke({
topic,
});
console.log(`[TitleNode] 文章标题:${title}`);
return {
title,
};
}
graph/content_node.ts
Using ReAct design go get result
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { SYSTEM_PROMPT, CONTENT_PROMPT } from "../prompt.ts";
import type { TArticle } from "../state.ts";
import {
type BaseMessageLike,
HumanMessage,
SystemMessage,
ToolMessage,
} from "@langchain/core/messages";
import { getChatGPT } from "../model.ts";
import tools from "../tools/index.ts";
export async function contentNode(state: TArticle): Promise<Partial<TArticle>> {
if (!state.topic) {
throw new Error("主题不能为空");
}
if (!state.title) {
throw new Error("标题不能为空");
}
const topic = state.topic;
const title = state.title;
const systemContent = SYSTEM_PROMPT.replace("{topic}", topic);
const userContent = CONTENT_PROMPT.replace("{title}", title);
const messages: BaseMessageLike[] = [
new SystemMessage(systemContent),
new HumanMessage(userContent),
];
const model = getChatGPT();
const modelWithTools = model.bindTools(tools);
// ReAct循环
// 1. 模型推理
// 2. action
while (true) {
const reply = await modelWithTools.invoke(messages);
messages.push(reply);
// 接下来就需要对 reply 进行一个判断,reply 的形态决定了是否退出当前的循环
/**
* reply是一个对象:
* 情况一:模型给你的回复是调用工具
* {
* content: "",
* tool_calls: [
* {name: "search", args: {query: "..."}, id: "call_xxx"}
* ]
* }
* 情况二:模型生成了最终的答复
* {
* content: "文章具体的内容.....",
* tool_calls: []
* }
*/
if (!reply.tool_calls || reply.tool_calls.length === 0) {
// 说明此时是情况二,不需要调用工具,模型已经生成了文章的完整内容
// 此节点可以结束了,可以进入下一个节点
const content = reply.content as string;
console.log(`文章的正文部分已经生成完毕,共 ${content.length} 字`);
return { content };
}
// 如果没有进入上面的 if,那么就调用工具
/**
* tool_calls: [
* {name: "search", args: {query: "..."}, id: "call_xxx", description: "xxxxx"},
* {name: "calc", args: {a: "...", b: "xxx"}, id: "call_xxx", description: "xxxxx"},
* ]
*/
for (const toolCall of reply.tool_calls) {
// 从当前的工具箱里面去寻找对应的工具
const selectedTool = tools.find((tool) => tool.name === toolCall.name);
if (selectedTool) {
// 从工具箱找到了相应的工具
// 既然找到了,那么就执行工具
const toolResult = await (selectedTool as any).invoke(toolCall.args);
console.log(
`[${toolCall.name}] 工具调用已经完成,工具调用结果为:${toolResult}`,
);
messages.push(
new ToolMessage({
content: toolResult,
tool_call_id: toolCall.id!,
name: toolCall.name,
}),
);
} else {
console.warn(`没有找到名为${toolCall.name}的工具`);
}
}
}
}
graph/image_node.ts
import type { TArticle } from "../state.ts";
import * as fs from "fs";
/**
*
* @param url 图片的下载地址
* @param filePath 图片的保存地址
*/
async function downloadImage(url: string, filePath: string) {
const response = await fetch(url);
if (!response.ok) throw new Error("下载图片失败");
const buffer = await response.arrayBuffer(); // 拿到一个图片流
fs.writeFileSync(filePath, Buffer.from(buffer));
}
export async function imageNode(state: TArticle): Promise<Partial<TArticle>> {
if (!state.summary) {
throw new Error("摘要不能为空");
}
const summary = state.summary;
try {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("缺少OpenAI API Key");
}
const response = await fetch(
"https://api.openai.com/v1/images/generations",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "dall-e-3",
prompt: summary,
n: 1,
size: "1024x1024",
}),
},
);
if (!response.ok) {
throw new Error(`图片生成失败,状态码为${response.status}`);
}
const data = await response.json();
if (!data.data || data.data.length === 0 || !data.data[0].url) {
throw new Error("图片生成失败,没有返回数据");
}
const imageUrl = data.data[0].url;
console.log(`图片生成成功,图片URL为:${imageUrl}`);
// 接下来我们需要将这个临时地址的图片赶紧下载下来
const imagePath = `./${state.title}.png`;
await downloadImage(imageUrl, imagePath); // 进行图片的下载
console.log(`文章插图已经下载完毕,保存在:${imagePath} 路径下`);
return {
image_path: imageUrl,
};
} catch (error) {
console.error("图片生成失败,错误信息为:", error);
throw error;
}
}
graph/agent.ts
import { END, START, StateGraph } from "@langchain/langgraph";
import { Schema, type TArticle } from "../state.ts";
import { imageNode } from "./image_node.ts";
import { summaryNode } from "./summary_node.ts";
import { titleNode } from "./title_node.ts";
import { contentNode } from "./content_node.ts";
import fs from "fs";
export function buildGraph() {
return new StateGraph(Schema)
.addNode("title_node", titleNode)
.addNode("summary_node", summaryNode)
.addNode("image_node", imageNode)
.addNode("content_node", contentNode)
.addEdge(START, "title_node")
.addEdge("title_node", "content_node")
.addEdge("content_node", "summary_node")
.addEdge("summary_node", "image_node")
.addEdge("image_node", END)
.compile();
}
export async function writeArticle(agent: any, topic: string) {
// 初始状态
const initState: TArticle = {
topic,
title: "",
content: "",
summary: "",
image_path: "",
};
console.log(`智能编辑已经开始撰写文章,文章的主题为:${topic}`);
return await agent.invoke(initState);
}
// 创建md文件
export function dumpMarkdown(state: TArticle) {
const { title, content, image_path } = state;
const filename = `./${title}.md`;
let mdContent = `# ${title}\n\n`;
mdContent += `${content}\n\n`;
mdContent += `\n\n`;
// 写入
fs.writeFileSync(filename, mdContent);
console.log(`文章已经生成完毕,保存至:${filename} 位置`);
}

浙公网安备 33010602011771号