审查工具调用¶
人机交互 (HIL) 是 代理系统 中的关键部分。一个常见的模式是在某些工具调用之后添加人机交互步骤。这些工具调用通常会导致函数调用或信息的保存。例如:
- 调用工具执行 SQL,然后由该工具运行
- 调用工具生成摘要,然后将其保存到图形状态
请注意,使用工具调用很常见,**无论是否实际调用工具**。
这里通常有几种不同的交互方式:
- 批准工具调用并继续
- 手动修改工具调用,然后继续
- 提供自然语言反馈,然后将其传递回代理,而不是继续
我们可以在 LangGraph 中使用 断点 来实现这一点:断点允许我们在特定步骤之前中断图形执行。在断点处,我们可以手动更新图形状态,选择上述三种选项之一。
设置¶
首先,我们需要安装所需的软件包
npm install @langchain/langgraph @langchain/anthropic @langchain/core
接下来,我们需要设置 Anthropic(我们将使用的 LLM)的 API 密钥
export ANTHROPIC_API_KEY=your-api-key
或者,我们可以设置 LangSmith 跟踪 的 API 密钥,这将为我们提供一流的可观察性。
export LANGCHAIN_TRACING_V2="true"
export LANGCHAIN_CALLBACKS_BACKGROUND="true"
export LANGCHAIN_API_KEY=your-api-key
简单用法¶
让我们设置一个非常简单的图形来实现这一点。首先,我们将有一个 LLM 调用来决定采取什么行动。然后,我们转到一个人类节点。这个节点实际上什么也不做——我们的想法是在这个节点之前中断,然后对状态应用任何更新。之后,我们检查状态,然后将路由返回 LLM 或正确的工具。
让我们看看它是如何工作的!
import {
MessagesAnnotation,
StateGraph,
START,
END,
MemorySaver
} from "@langchain/langgraph";
import { ChatAnthropic } from "@langchain/anthropic";
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { type AIMessage, isAIMessage } from '@langchain/core/messages';
import { ToolNode } from '@langchain/langgraph/prebuilt'
const getWeather = tool((input) => {
const city = input.city;
console.log("----");
console.log(`Searching for: ${city}`);
console.log("----");
return "Sunny!";
}, {
name: 'get_weather',
description: 'Call to get the current weather.',
schema: z.object({
city: z.string().describe("City to get the weather for."),
})
})
const tools = [getWeather];
const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20240620" });
const modelWithTools = model.bindTools([getWeather]);
const callLLM = async (state: typeof MessagesAnnotation.State) => {
const response = await modelWithTools.invoke(state.messages);
return { messages: [response] };
}
const humanReviewNode = async (state: typeof MessagesAnnotation.State) => {
// Node waits for human input and then goes to next node
return { messages: [] };
};
function routeAfterLLM(state: typeof MessagesAnnotation.State): typeof END | "human_review_node" {
const lastMessage: AIMessage = state.messages[state.messages.length - 1];
if (lastMessage.tool_calls?.length === 0) {
return END;
} else {
return "human_review_node";
}
}
function routeAfterHuman(state: typeof MessagesAnnotation.State): "tools" | "llm" {
if (isAIMessage(state.messages[state.messages.length - 1])) {
return "tools";
} else {
return "llm";
}
}
// State will be { messages: Messages[] }
const workflow = new StateGraph(MessagesAnnotation)
// Define the two nodes we will cycle between
.addNode("llm", callLLM)
.addNode("tools", new ToolNode(tools))
.addNode("human_review_node", humanReviewNode)
.addEdge(START, "llm")
.addConditionalEdges(
"llm",
routeAfterLLM,
["human_review_node", END]
)
.addConditionalEdges(
"human_review_node",
routeAfterHuman,
["tools", "llm"]
)
.addEdge("tools", "llm");
// Setup memory
const memory = new MemorySaver();
const graph = workflow.compile({ interruptBefore: ["human_review_node"], checkpointer: memory });
import * as tslab from "tslab";
const drawableGraph = graph.getGraph();
const image = await drawableGraph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
没有审查的示例¶
让我们看看没有审查要求(因为没有调用工具)的情况
let inputs = { messages: [{ role: "user", content: "hi!" }] };
let config = { configurable: { thread_id: "1" }, streamMode: "values" as const };
let stream = await graph.stream(inputs, config);
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
================================ human Message (1) ================================= hi! ================================ ai Message (1) ================================= Hello! Welcome! How can I assist you today? Is there anything specific you'd like to know or discuss?
如果我们检查状态,我们可以看到它已经完成,因为没有后续步骤要执行
let state = await graph.getState(config);
console.log(state.next);
[]
批准工具的示例¶
现在让我们看看批准工具调用看起来是什么样的
inputs = { messages: [{ role: "user", content: "what's the weather in SF?" }] };
config = { configurable: { thread_id: "2" }, streamMode: "values" as const };
stream = await graph.stream(inputs, config);
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
================================ human Message (1) ================================= what's the weather in SF? ================================ ai Message (1) ================================= [ { type: 'text', text: 'To get the weather in San Francisco, I can use the get_weather function. Let me fetch that information for you.' }, { type: 'tool_use', id: 'toolu_01TZ9HWpeQbgRnkoV5zhLAoB', name: 'get_weather', input: { city: 'San Francisco' } } ]
如果我们现在检查,我们可以看到它正在等待人工审查
state = await graph.getState(config);
console.log(state.next);
[ 'human_review_node' ]
要批准工具调用,我们只需继续执行线程,无需进行任何编辑。为此,我们只需创建一个没有输入的新运行。
stream = await graph.stream(null, config);
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
================================ ai Message (1) ================================= [ { type: 'text', text: 'To get the weather in San Francisco, I can use the get_weather function. Let me fetch that information for you.' }, { type: 'tool_use', id: 'toolu_01TZ9HWpeQbgRnkoV5zhLAoB', name: 'get_weather', input: { city: 'San Francisco' } } ] ---- Searching for: San Francisco ---- ================================ tool Message (1) ================================= Sunny! ================================ ai Message (1) ================================= Great news! The current weather in San Francisco is sunny. It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?
编辑工具调用¶
现在假设我们要编辑工具调用。例如,更改某些参数(甚至更改调用的工具!),然后执行该工具。
inputs = { messages: [{ role: "user", content: "what's the weather in SF?" }] };
config = { configurable: { thread_id: "3" }, streamMode: "values" as const };
stream = await graph.stream(inputs, config);
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
================================ human Message (1) ================================= what's the weather in SF? ================================ ai Message (1) ================================= [ { type: 'text', text: "Certainly! I can help you check the weather in San Francisco. To get this information, I'll need to use the weather tool. Let me do that for you right away." }, { type: 'tool_use', id: 'toolu_01PCbjHE6kxf7aXPTEFbkaZz', name: 'get_weather', input: { city: 'San Francisco' } } ]
如果我们现在检查,我们可以看到它正在等待人工审查
state = await graph.getState(config);
console.log(state.next);
[ 'human_review_node' ]
要编辑工具调用,我们首先需要更新状态。我们可以通过传入具有与要覆盖的消息相同**ID**的消息来完成此操作。这将起到**替换**旧消息的效果。请注意,这只有在我们使用的**reducer** 替换具有相同 ID 的消息时才有可能。
console.log("Current Tool Call ID:");
const currentContent = state.values["messages"][state.values["messages"].length-1].content;
const currrentID = state.values["messages"][state.values["messages"].length-1].id;
const toolCallID = state.values["messages"][state.values["messages"].length-1].tool_calls[0]["id"];
console.log(toolCallID);
let newMessage = {
role: "assistant",
content: currentContent,
tool_calls: [
{
id: toolCallID,
name: "get_weather",
args: {
city: "San Francisco, USA"
}
}
],
id: currrentID
};
// Update the state with the correct config, values, and specify the node we are acting as
await graph.updateState(config, { messages: [newMessage] }, "human_review_node");
stream = await graph.stream(null, config);
// Let's now continue executing from here
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
Current Tool Call ID: toolu_01PCbjHE6kxf7aXPTEFbkaZz ---- Searching for: San Francisco, USA ---- ================================ tool Message (1) ================================= Sunny! ================================ ai Message (1) ================================= Great news! The current weather in San Francisco is sunny. It's a beautiful day in the city by the bay. Is there anything else you'd like to know about the weather or any other information I can help you with?
向工具调用提供反馈¶
有时,您可能不想执行工具调用,但您也不想让用户手动修改工具调用。在这种情况下,可能最好从用户那里获取自然语言反馈。然后,您可以将这些反馈作为工具调用的模拟**结果**插入。
有多种方法可以做到这一点
- 您可以向状态添加一条新消息(代表工具调用的“结果”)
- 您可以向状态添加两条新消息——一条代表工具调用的“错误”,另一条 HumanMessage 代表反馈
这两者都类似于向状态添加消息。主要区别在于human_node
之后的逻辑以及它如何处理不同类型的消息。
在本例中,我们将只添加一条代表反馈的工具调用消息。让我们看看它是如何工作的!
inputs = { messages: [{ role: "user", content: "what's the weather in SF?" }] };
config = { configurable: { thread_id: "4" }, streamMode: "values" as const };
stream = await graph.stream(inputs, config);
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
================================ human Message (1) ================================= what's the weather in SF?
================================ ai Message (1) ================================= [ { type: 'text', text: "Certainly! I can help you check the weather in San Francisco. To get this information, I'll need to use the weather tool available to me. Let me fetch that data for you." }, { type: 'tool_use', id: 'toolu_01NMNPP7upggjzNqh8m2cKGm', name: 'get_weather', input: { city: 'San Francisco' } } ]
如果我们现在检查,我们可以看到它正在等待人工审查
state = await graph.getState(config);
console.log(state.next);
[ 'human_review_node' ]
要提供有关工具调用的反馈,我们首先需要更新状态。我们可以通过传入具有与要响应的工具调用的相同**工具调用 ID**的消息来完成此操作。请注意,这与上面的**ID**不同。
console.log("Current Tool Call ID:");
const stateMessages = state.values.messages;
const toolCallId = stateMessages[stateMessages.length - 1].tool_calls[0].id;
console.log(toolCallId);
const toolMessage = {
role: "tool",
name: "get_weather",
content: "User requested changes: pass in the most likely country this city is referring to as well",
tool_call_id: toolCallId,
};
// Update the state with the correct config, values, and specify the node we are acting as
await graph.updateState(config, { messages: [toolMessage] }, "human_review_node");
stream = await graph.stream(null, config);
// Let's now continue executing from here
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
8:5 - Type '{ role: string; name: string; content: string; tool_call_id: any; }' is not assignable to type '{ role: string; content: any; tool_calls: { id: any; name: string; args: { city: string; }; }[]; id: any; }'. 8:5 - Object literal may only specify known properties, and 'name' does not exist in type '{ role: string; content: any; tool_calls: { id: any; name: string; args: { city: string; }; }[]; id: any; }'.
我们可以看到,我们现在到达了另一个断点 - 因为它回到了模型并获得了对要调用的内容的全新预测。现在让我们批准这个并继续。
state = await graph.getState(config);
console.log(state.next);
stream = await graph.stream(null, config);
for await (const event of stream) {
const recentMsg = event.messages[event.messages.length - 1];
console.log(`================================ ${recentMsg._getType()} Message (1) =================================`)
console.log(recentMsg.content);
}
[ 'human_review_node' ] ================================ ai Message (1) ================================= [ { type: 'text', text: 'I apologize for the oversight. It seems the function requires more specific information. Let me try again with a more detailed request.' }, { type: 'tool_use', id: 'toolu_01Q92V4EztnVv6qsUHABGP8V', name: 'get_weather', input: { city: 'San Francisco, USA' } } ] ---- Searching for: San Francisco, USA ---- [ ToolMessage { "content": "Sunny!", "name": "get_weather", "additional_kwargs": {}, "response_metadata": {}, "tool_call_id": "toolu_01Q92V4EztnVv6qsUHABGP8V" } ] ================================ tool Message (1) ================================= Sunny! ================================ ai Message (1) ================================= Great news! Based on the information I've received, the weather in San Francisco, USA is currently sunny! Is there anything else you'd like to know about the weather in San Francisco or any other location?