如何让 Agent 以结构化格式响应¶
典型的 ReAct Agent 提示 LLM 以两种格式之一响应:调用函数(~ JSON)以使用工具,或对话文本以响应用户。
如果您的 Agent 连接到结构化(甚至生成式)UI,或者它正在与另一个 Agent 或软件进程通信,您可能希望它以特定的结构化格式响应。
在本例中,我们将构建一个会话式 ReAct Agent,它以特定格式响应。我们将通过使用工具调用来实现这一点。当您想强制 Agent 的响应采用特定格式时,这非常有用。在本例中,我们将要求它像天气预报员一样响应,在单独的机器可读字段中返回温度和其他信息。
设置¶
首先,我们需要安装所需的软件包
接下来,我们需要为 OpenAI(我们将使用的 LLM)设置 API 密钥。
可选地,我们可以为 LangSmith 追踪设置 API 密钥,这将为我们提供一流的可观测性。
// process.env.LANGCHAIN_API_KEY = "ls...";
process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true";
process.env.LANGCHAIN_TRACING_V2 = "true";
process.env.LANGCHAIN_PROJECT = "Respond in Format: LangGraphJS";
设置状态¶
import { Annotation, messagesStateReducer } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
const GraphState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: messagesStateReducer,
}),
});
设置工具¶
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const searchTool = tool((_) => {
// This is a placeholder, but don't tell the LLM that...
return "67 degrees. Cloudy with a chance of rain.";
}, {
name: "search",
description: "Call to surf the web.",
schema: z.object({
query: z.string().describe("The query to use in your search."),
}),
});
const tools = [searchTool];
我们现在可以将这些工具包装在 ToolNode 中。
import { ToolNode } from "@langchain/langgraph/prebuilt";
const toolNode = new ToolNode<typeof GraphState.State>(tools);
设置模型¶
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({
temperature: 0,
model: "gpt-4o",
});
完成此操作后,我们应确保模型知道它可以调用这些工具。我们可以通过将 LangChain 工具绑定到模型类来实现这一点。
我们还想为语言模型定义一个响应模式,并将其作为工具绑定到模型。我们的想法是,当模型准备好响应时,它将调用这个最终工具,并根据我们想要的模式填充参数。我们将从图中返回,而不是调用工具。
因为我们只打算使用这个最终工具来指导模型最终响应的模式,所以我们将使用模拟函数声明它
import { tool } from "@langchain/core/tools";
const Response = z.object({
temperature: z.number().describe("the temperature"),
other_notes: z.string().describe("any other notes about the weather"),
});
const finalResponseTool = tool(async () => "mocked value", {
name: "Response",
description: "Always respond to the user using this tool.",
schema: Response
})
const boundModel = model.bindTools([
...tools,
finalResponseTool
]);
定义节点¶
import { AIMessage } from "@langchain/core/messages";
import { RunnableConfig } from "@langchain/core/runnables";
// Define the function that determines whether to continue or not
const route = (state: typeof GraphState.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 || lastMessage.tool_calls.length === 0) {
return "__end__";
}
// Otherwise if there is, we need to check what type of function call it is
if (lastMessage.tool_calls[0].name === "Response") {
return "__end__";
}
// Otherwise we continue
return "tools";
};
// Define the function that calls the model
const callModel = async (
state: typeof GraphState.State,
config?: RunnableConfig,
) => {
const { messages } = state;
const response = await boundModel.invoke(messages, config);
// We return an object, because this will get added to the existing list
return { messages: [response] };
};
定义图¶
import { StateGraph } from "@langchain/langgraph";
// Define a new graph
const workflow = new StateGraph(GraphState)
.addNode("agent", callModel)
.addNode("tools", toolNode)
.addEdge("__start__", "agent")
.addConditionalEdges(
// First, we define the start node. We use `agent`.
// This means these are the edges taken after the `agent` node is called.
"agent",
// Next, we pass in the function that will determine which node is called next.
route,
// We supply a map of possible response values to the conditional edge
// to make it possible to draw a visualization of the graph.
{
__end__: "__end__",
tools: "tools",
}
)
// We now add a normal edge from `tools` to `agent`.
// This means that after `tools` is called, `agent` node is called next.
.addEdge("tools", "agent");
// Finally, we compile it!
// This compiles it into a LangChain Runnable,
// meaning you can use it as you would any other runnable
const app = workflow.compile();
import * as tslab from "tslab";
const graph = app.getGraph();
const image = await graph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();
await tslab.display.png(new Uint8Array(arrayBuffer));
使用它!¶
我们现在可以使用它了!现在它公开了与所有其他 LangChain Runnables 相同的接口。
import { HumanMessage, isAIMessage } from "@langchain/core/messages";
const prettyPrint = (message: BaseMessage) => {
let txt = `[${message._getType()}]: ${message.content}`;
if (
isAIMessage(message) && message?.tool_calls?.length
) {
const tool_calls = message?.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")],
};
const stream = await app.stream(inputs, { streamMode: "values" });
for await (const output of stream) {
const { messages } = output;
prettyPrint(messages[messages.length - 1]);
console.log("\n---\n");
}
[human]: what is the weather in sf
---
[ai]:
Tools:
- search({"query":"current weather in San Francisco"})
---
[tool]: 67 degrees. Cloudy with a chance of rain.
---
[ai]:
Tools:
- Response({"temperature":67,"other_notes":"Cloudy with a chance of rain."})
---
部分流式 JSON¶
如果我们想在结构化输出可用时立即对其进行流式传输,我们可以使用 .streamEvents()
方法。我们将聚合发出的 on_chat_model_events
并检查 name 字段。一旦我们检测到模型正在调用最终输出工具,我们就可以开始记录相关的块。
这是一个例子
import { concat } from "@langchain/core/utils/stream";
const eventStream = await app.streamEvents(inputs, { version: "v2" });
let aggregatedChunk;
for await (const { event, data } of eventStream) {
if (event === "on_chat_model_stream") {
const { chunk } = data;
if (aggregatedChunk === undefined) {
aggregatedChunk = chunk;
} else {
aggregatedChunk = concat(aggregatedChunk, chunk);
}
const currentToolCalls = aggregatedChunk.tool_calls;
if (
currentToolCalls.length === 0 ||
currentToolCalls[0].name === "" ||
!finalResponseTool.name.startsWith(currentToolCalls[0].name)
) {
// No tool calls or a different tool call in the message,
// so drop what's currently aggregated and start over
aggregatedChunk = undefined;
} else if (currentToolCalls[0].name === finalResponseTool.name) {
// Now we're sure that this event is part of the final output!
// Log the partially aggregated args.
console.log(aggregatedChunk.tool_call_chunks[0].args);
// You can also log the raw args instead:
// console.log(chunk.tool_call_chunks);
console.log("---");
}
}
}
// Final aggregated tool call
console.log(aggregatedChunk.tool_calls);
---
{"
---
{"temperature
---
{"temperature":
---
{"temperature":67
---
{"temperature":67,"
---
{"temperature":67,"other
---
{"temperature":67,"other_notes
---
{"temperature":67,"other_notes":"
---
{"temperature":67,"other_notes":"Cloud
---
{"temperature":67,"other_notes":"Cloudy
---
{"temperature":67,"other_notes":"Cloudy with
---
{"temperature":67,"other_notes":"Cloudy with a
---
{"temperature":67,"other_notes":"Cloudy with a chance
---
{"temperature":67,"other_notes":"Cloudy with a chance of
---
{"temperature":67,"other_notes":"Cloudy with a chance of rain
---
{"temperature":67,"other_notes":"Cloudy with a chance of rain."
---
{"temperature":67,"other_notes":"Cloudy with a chance of rain."}
---
{"temperature":67,"other_notes":"Cloudy with a chance of rain."}
---
[
{
name: 'Response',
args: { temperature: 67, other_notes: 'Cloudy with a chance of rain.' },
id: 'call_oOhNx2SdeelXn6tbenokDtkO',
type: 'tool_call'
}
]