如何审查工具调用¶
人机交互(HIL)对代理系统至关重要。常见的模式是在某些工具调用后添加一个步骤来引入人类干预。这些工具调用通常会导致函数调用或信息保存。例如:
- 执行SQL的工具调用,该调用将由工具执行
- 生成摘要的工具调用,该摘要将被保存到图的状态中
请注意,无论是否实际调用工具,使用工具调用都是常见的做法。
在这里,您通常可能希望进行以下几种不同的交互:
- 批准工具调用并继续
- 手动修改工具调用然后继续
- 给出自然语言反馈,然后将其传递回代理
我们可以在LangGraph中使用interrupt()
函数来实现这些功能。interrupt
允许我们在收集用户输入后停止图的执行,并在收集输入后继续执行:
def human_review_node(state) -> Command[Literal["call_llm", "run_tool"]]:
# 这是我们将通过Command(resume=<human_review>)提供的值
human_review = interrupt(
{
"question": "这是正确的吗?",
# 展示工具调用以供审核
"tool_call": tool_call
}
)
review_action, review_data = human_review
# 批准工具调用并继续
if review_action == "continue":
return Command(goto="run_tool")
# 手动修改工具调用然后继续
elif review_action == "update":
...
updated_msg = get_updated_msg(review_data)
return Command(goto="run_tool", update={"messages": [updated_message]})
# 给出自然语言反馈,然后将其传递回代理
elif review_action == "feedback":
...
feedback_msg = get_feedback_msg(review_data)
return Command(goto="call_llm", update={"messages": [feedback_msg]})
设置¶
我们不会展示托管图表的完整代码,但你可以在这里查看此处。一旦该图表被托管,我们就准备好调用它并等待用户输入。
SDK 初始化¶
首先,我们需要设置客户端以便能够与托管的图表通信:
示例:批准工具调用¶
首先,让我们运行一个需要批准的工具调用输入的代理:
const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] };
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "updates"
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
输出:
{'call_llm': {'messages': [{'content': [{'text': "I'll help you check the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01Tdfufy4nZYXMbVZvgyNbhc', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 379, 'output_tokens': 66}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-a33434b2-f5ca-40c6-98e2-6288d349d4ce-0', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 379, 'output_tokens': 66, 'total_tokens': 445, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
{'__interrupt__': [{'value': {'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'San Francisco'}, 'id': 'toolu_01142G3woscA8JjFTLdqymtn', 'type': 'tool_call'}}, 'resumable': True, 'ns': ['human_review_node:9caf42cf-1371-7213-a331-e6fe5d026be8'], 'when': 'during'}]}
为了批准工具调用,我们需要让human_review_node
知道我们为节点中定义的human_review
变量使用什么值。我们可以通过调用图并提供一个Command(resume=<human_review>)
输入来提供此值。由于我们正在批准工具调用,我们将提供resume
值为{"action": "continue"}
以导航到run_tool
节点:
输出:
{'human_review_node': None}
{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01142G3woscA8JjFTLdqymtn'}]}}
{'call_llm': {'messages': [{'content': "According to the search, it's sunny in San Francisco right now!", 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01JJE9AtT4a9Lob91RRiW9rU', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 458, 'output_tokens': 18}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-5e8d80b5-c46a-4aad-af37-b01f8bb15963-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 458, 'output_tokens': 18, 'total_tokens': 476, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
编辑工具调用¶
现在我们假设要编辑工具调用。例如,更改一些参数(甚至更改调用的工具!),然后执行该工具。
const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] };
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "updates",
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
为了实现这一点,我们将使用 Command
并设置不同的恢复值为 {"action": "update", "data": <tool call args>}
。这将完成以下操作:
- 结合现有工具调用与用户提供的工具调用参数,并使用新的工具调用更新现有的AI消息。
- 导航到带有更新后的AI消息的
run_tool
节点并继续执行。
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
command: {
resume: { "action": "update", "data": { "city": "San Francisco, USA" } }
},
streamMode: "updates"
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"command\": {
\"resume\": { \"action\": \"update\", \"data\": { \"city\": \"San Francisco, USA\" } }
},
\"stream_mode\": [
\"updates\"
]
}"
输出:
{'human_review_node': {'messages': [{'role': 'ai', 'content': [{'text': "I'll help you check the weather in San Francisco.", 'type': 'text'}, {'id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa', 'input': {'city': 'San Francisco'}, 'name': 'weather_search', 'type': 'tool_use'}], 'tool_calls': [{'id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa', 'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}}], 'id': 'run-b07f0c35-4e93-43a5-9b48-363767ada3ca-0'}]}}
{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_016L4EDPcaQRzzZxiB4Wq2wa'}]}}
{'call_llm': {'messages': [{'content': "According to the search, it's sunny in San Francisco right now!", 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01De5HurjNUMwMUpfRtMLbX1', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 460, 'output_tokens': 18}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-85e2aaaa-6f61-4fa0-b594-b6e57129d7e7-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 460, 'output_tokens': 18, 'total_tokens': 478, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
给工具调用提供反馈¶
有时,您可能不想执行一个工具调用,但也不想让用户手动修改该工具调用。在这种情况下,最好从用户那里获取自然语言反馈。然后可以将此反馈插入为工具调用的模拟 RESULT。
有多种方法可以做到这一点:
- 您可以在状态中添加一条新的消息(代表工具调用的“结果”)。
- 您也可以在状态中添加两条新消息——一条代表工具调用的“错误”,另一条代表用户的反馈。
这两种方法都涉及向状态中添加消息,主要区别在于 human_review_node
之后的逻辑以及如何处理不同类型的消息。
在这个示例中,我们将只添加一条代表反馈的新工具调用(参见 human_review_node
实现)。让我们看看实际操作!
const input = { "messages": [{ "role": "user", "content": "what's the weather in sf?" }] };
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
input: input,
streamMode: "updates"
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
为此,我们将使用 Command
并设置不同的恢复值为 {"action": "feedback", "data": <反馈字符串>}
。这将执行以下操作:
- 创建一个新的工具消息,结合来自 LLM 的现有工具调用和用户提供的反馈作为内容
- 导航到
call_llm
节点,并继续执行更新后的工具消息
from langgraph_sdk.schema import Command
async for chunk in client.runs.stream(
thread["thread_id"],
assistant_id,
command=Command(
resume={
"action": "feedback",
"data": "User requested changes: use <city, country> format for location"
}
),
stream_mode="updates",
):
if chunk.data and chunk.event != "metadata":
print(chunk.data)
const streamResponse = client.runs.stream(
thread["thread_id"],
assistantId,
{
command: {
resume: {
"action": "feedback",
"data": "User requested changes: use <city, country> format for location"
}
},
streamMode: "updates"
}
);
for await (const chunk of streamResponse) {
if (chunk.data && chunk.event !== "metadata") {
console.log(chunk.data);
}
}
curl --request POST \
--url <DEPLOYMENT_URL>/threads/<THREAD_ID>/runs/stream \
--header 'Content-Type: application/json' \
--data "{
\"assistant_id\": \"agent\",
\"command\": {
\"resume\": { \"action\": \"feedback\", \"data\": \"User requested changes: use <city, country> format for location\" }
},
\"stream_mode\": [
\"updates\"
]
}"
输出:
{'human_review_node': {'messages': [{'role': 'tool', 'content': 'User requested changes: use <city, country> format for location', 'name': 'weather_search', 'tool_call_id': 'toolu_01RkPHCjpfoUvPAktaq4Cqhm'}]}}
{'call_llm': {'messages': [{'content': [{'text': 'Let me try that again with the correct format:', 'type': 'text'}, {'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'input': {'city': 'San Francisco, USA'}, 'name': 'weather_search', 'type': 'tool_use'}], 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_01EBan969yY5f6iGk6sPgKcj', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 469, 'output_tokens': 68}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-64bbc255-d126-4db0-8ae5-3197cf29bed1-0', 'example': False, 'tool_calls': [{'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 469, 'output_tokens': 68, 'total_tokens': 537, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}
{'__interrupt__': [{'value': {'question': 'Is this correct?', 'tool_call': {'name': 'weather_search', 'args': {'city': 'San Francisco, USA'}, 'id': 'toolu_01Rdrag6cVufHZG26BwVaiE7', 'type': 'tool_call'}}, 'resumable': True, 'ns': ['human_review_node:e9856878-e28c-5dd1-d353-4d83aa1a3a2b'], 'when': 'during'}]}
我们可以看到现在又得到了另一个中断——因为它返回模型并得到了一个新的预测。现在我们批准这个预测并继续执行。
输出:
{'human_review_node': None}
{'run_tool': {'messages': [{'role': 'tool', 'name': 'weather_search', 'content': 'Sunny!', 'tool_call_id': 'toolu_01Rdrag6cVufHZG26BwVaiE7'}]}}
{'call_llm': {'messages': [{'content': 'The weather in San Francisco is sunny!', 'additional_kwargs': {}, 'response_metadata': {'id': 'msg_013WTDHhbg8WiYLiQ9n2CaTk', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 550, 'output_tokens': 12}, 'model_name': 'claude-3-5-sonnet-20241022'}, 'type': 'ai', 'name': None, 'id': 'run-b6c815f0-989a-47cf-b150-33e3bbc4eab7-0', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 550, 'output_tokens': 12, 'total_tokens': 562, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}}]}}