如何让代理直接返回工具结果¶
一个典型的 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 模式,以替代工具自动推断的模式。
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
中,节点可以是一个函数或一个 runnable。为此,我们需要两个主要节点:
- 代理:负责决定采取哪些行动(如果需要)。
- 调用工具的函数:如果代理决定采取行动,该节点将执行该行动。
我们还需要定义一些边。其中一些边可能是条件性的。它们之所以是条件性的,是因为根据节点的输出,可能会选择几条路径中的一条。直到该节点运行(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 runnable 相同的接口。
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
节点后停止了!