如何让代理直接返回工具结果¶
一个典型的ReAct循环遵循用户 -> 助手 -> 工具 -> 助手 ... -> 用户。在某些情况下,工具完成操作后你不需要再次调用LLM,用户可以直接查看结果。
在这个示例中,我们将构建一个对话式ReAct代理,LLM可以选择将工具调用的结果作为最终答案返回。这在某些情况下非常有用,例如当你的工具能够生成可接受的最终答案时,并且你希望LLM来判断何时应该这样做。
设置¶
首先我们需要安装所需的包
接下来,我们需要设置OpenAI(我们将使用的LLM)的API密钥。此外,我们还可以选择设置LangSmith跟踪的API密钥,这将为我们提供一流的可观察性。
// process.env.OPENAI_API_KEY = "sk_...";
// Optional, add tracing in LangSmith
// process.env.LANGCHAIN_API_KEY = "ls__..."
process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true";
process.env.LANGCHAIN_TRACING_V2 = "true";
process.env.LANGCHAIN_PROJECT = "Direct Return: LangGraphJS";
设置工具¶
我们首先定义要使用的工具。对于这个简单的例子,我们将使用一个简单的占位符“搜索引擎”。然而,创建自己的工具非常容易——请参阅此处的文档,了解如何操作。
为了添加“return_direct”选项,我们将创建一个自定义zod schema,以替代工具自动推断的schema。
import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";
const SearchTool = z.object({
query: z.string().describe("query to look up online"),
// **IMPORTANT** We are adding an **extra** field here
// that isn't used directly by the tool - it's used by our
// graph instead to determine whether or not to return the
// result directly to the user
return_direct: z.boolean()
.describe(
"Whether or not the result of this should be returned directly to the user without you seeing what it is",
)
.default(false),
});
const searchTool = new DynamicStructuredTool({
name: "search",
description: "Call to surf the web.",
// We are overriding the default schema here to
// add an extra field
schema: SearchTool,
func: async ({}: { query: string }) => {
// This is a placeholder for the actual implementation
// Don't let the LLM know this though 😊
return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈.";
},
});
const tools = [searchTool];
我们现在可以将这些工具封装在一个ToolNode
中。这是一个预构建的节点,它接收LangChain聊天模型生成的工具调用,并调用该工具,然后返回输出。
设置模型¶
现在我们需要加载我们想要使用的聊天模型。\重要的是,它应该满足两个标准
- 它应该能与消息配合使用。我们将所有代理状态都以消息的形式表示,因此它需要能够很好地与消息配合使用。
- 它应该支持工具调用。
注意:这些模型要求并非使用LangGraph的必要条件——它们只是此示例的必要条件。
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
temperature: 0,
model: "gpt-3.5-turbo",
});
// This formats the tools as json schema for the model API.
// The model then uses this like a system prompt.
const boundModel = model.bindTools(tools);
定义代理状态¶
langgraph
中主要的图类型是StateGraph。
这个图由一个状态对象参数化,它将该状态对象传递给每个节点。然后每个节点返回操作来更新该状态。这些操作可以SET状态上的特定属性(例如,覆盖现有值),或者ADD到现有属性。是设置还是添加在您构建图时使用的状态对象中表示。
对于这个例子,我们将跟踪的状态将只是一个消息列表。我们希望每个节点都将消息添加到该列表中。因此,我们将状态定义如下
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
const AgentState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (x, y) => x.concat(y),
}),
});
定义节点¶
现在我们需要在图中定义几个不同的节点。在 langgraph
中,节点可以是函数或可运行对象。为此我们需要两个主要的节点:
- 代理:负责决定(如果需要)采取哪些行动。
- 一个调用工具的函数:如果代理决定采取行动,则此节点将执行该行动。
我们还需要定义一些边。其中一些边可能是条件性的。它们是条件性的原因在于,根据节点的输出,可能会选择多条路径中的一条。在节点运行之前(LLM决定),选择哪条路径是未知的。
- 条件边:在代理被调用后,我们应该:a. 如果代理说要执行动作,那么应该调用调用工具的函数;b. 如果代理说它已完成,那么它应该结束。
- 普通边:在工具被调用后,它应该总是返回到代理,由代理决定接下来做什么。
让我们定义节点,以及一个决定采取何种条件边的函数。
import { RunnableConfig } from "@langchain/core/runnables";
import { END } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";
// Define the function that determines whether to continue or not
const shouldContinue = (state: typeof AgentState.State) => {
const { messages } = state;
const lastMessage = messages[messages.length - 1] as AIMessage;
// If there is no function call, then we finish
if (!lastMessage?.tool_calls?.length) {
return END;
} // Otherwise if there is, we check if it's suppose to return direct
else {
const args = lastMessage.tool_calls[0].args;
if (args?.return_direct) {
return "final";
} else {
return "tools";
}
}
};
// Define the function that calls the model
const callModel = async (state: typeof AgentState.State, config?: RunnableConfig) => {
const messages = state.messages;
const response = await boundModel.invoke(messages, config);
// We return an object, because this will get added to the existing list
return { messages: [response] };
};
定义图¶
现在我们可以将它们整合在一起并定义图!
import { START, StateGraph } from "@langchain/langgraph";
// Define a new graph
const workflow = new StateGraph(AgentState)
// Define the two nodes we will cycle between
.addNode("agent", callModel)
// Note the "action" and "final" nodes are identical!
.addNode("tools", toolNode)
.addNode("final", toolNode)
// Set the entrypoint as `agent`
.addEdge(START, "agent")
// We now add a conditional edge
.addConditionalEdges(
// First, we define the start node. We use `agent`.
"agent",
// Next, we pass in the function that will determine which node is called next.
shouldContinue,
)
// We now add a normal edge from `tools` to `agent`.
.addEdge("tools", "agent")
.addEdge("final", END);
// Finally, we compile it!
const app = workflow.compile();
使用它!¶
现在我们可以使用它了!这现在暴露出与所有其他 LangChain 可运行对象相同的接口。
import { HumanMessage, isAIMessage } from "@langchain/core/messages";
const prettyPrint = (message: BaseMessage) => {
let txt = `[${message._getType()}]: ${message.content}`;
if (
isAIMessage(message) && (message as AIMessage)?.tool_calls?.length || 0 > 0
) {
const tool_calls = (message as AIMessage)?.tool_calls
?.map((tc) => `- ${tc.name}(${JSON.stringify(tc.args)})`)
.join("\n");
txt += ` \nTools: \n${tool_calls}`;
}
console.log(txt);
};
const inputs = { messages: [new HumanMessage("what is the weather in sf")] };
for await (const output of await app.stream(inputs, { streamMode: "values" })) {
const lastMessage = output.messages[output.messages.length - 1];
prettyPrint(lastMessage);
console.log("-----\n");
}
[human]: what is the weather in sf
-----
[ai]:
Tools:
- search({"query":"weather in San Francisco"})
-----
[tool]: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.
-----
[ai]: The weather in San Francisco is sunny.
-----
const inputs2 = {
messages: [
new HumanMessage(
"what is the weather in sf? return this result directly by setting return_direct = True",
),
],
};
for await (
const output of await app.stream(inputs2, { streamMode: "values" })
) {
const lastMessage = output.messages[output.messages.length - 1];
prettyPrint(lastMessage);
console.log("-----\n");
}
[human]: what is the weather in sf? return this result directly by setting return_direct = True
-----
[ai]:
Tools:
- search({"query":"weather in San Francisco","return_direct":true})
-----
[tool]: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.
-----
tools
节点后停止了!