参考资料
https://www.bilibili.com/video/BV1dw526tEMA
https://github.com/Wood-Q/MokioAgent
模型工具调用原理
函数工具调用是大模型获得/控制计算机资源的基本实现方式,相当于给大脑装了手和脚
对于大脑与四肢的通信方式,我们使用模式匹配,即利用正则/json格式化输出的方式,将字符串转换成对象,调用特定工具
下面是一段天气查询的代码:
DEFAULT_USER_PROMPT = "帮我查一下 杭州 的天气"
SYSTEM_PROMPT = """
你是一个助手。
系统里有一个工具叫 get_weather,用于查询天气。
当用户询问天气时,不要直接回答,而是根据用户输入的城市中文将其转换成拼音(首字母大写)然后作为参数。
你必须严格输出下面的格式,不要输出额外内容:
<Tool>get_weather</Tool>
<Args>{"city":"Beijing"}</Args>
如果用户不需要查询天气,就直接输出普通文本。
""".strip()
# 调用天气工具获得天气信息
def get_weather(city: str) -> str:
weather = {
"Beijing": "晴天,25 度",
"Shanghai": "多云,28 度",
"Hangzhou": "小雨,22 度",
}
return f"{city} 的天气是:{weather.get(city, '未知天气')}"
# 加载模型
def load_llm() -> ChatOpenAI:
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
return ChatOpenAI(
model=os.getenv("MODEL", "deepseek-v4-flash"),
base_url=os.getenv("BASE_URL", "https://api.deepseek.com"),
api_key=os.getenv("API_KEY", "sk-1dd85ee7aa0f48fe85399527637d1b9e"),
temperature=0,
)
# 匹配 toolcall字符串 识别成 字典对象 信息
def parse_tool_call(text: str) -> dict[str, object] | None:
tool_match = re.search(r"<Tool>(.*?)</Tool>", text, re.DOTALL)
args_match = re.search(r"<Args>(.*?)</Args>", text, re.DOTALL)
if not tool_match:
return None
args = json.loads(args_match.group(1)) if args_match else {}
return {
"tool": tool_match.group(1).strip(),
"args": args,
}
def main() -> None:
user_prompt = " ".join(sys.argv[1:]).strip() or DEFAULT_USER_PROMPT
print("=== 02. Prompt 协议版 ToolCall ===")
print("\n用户请求:")
print(user_prompt)
response = load_llm().invoke(
[
SystemMessage(content=SYSTEM_PROMPT),
HumanMessage(content=user_prompt),
]
)
model_text = str(response.content)
print("\n模型原始输出:")
print(model_text)
call = parse_tool_call(model_text)
if call is None:
print("\n没有工具调用,模型返回的是普通文本。")
return
print("\n解析后的工具请求:")
print(f"tool_name = {call['tool']}")
print(f"tool_args = {call['args']}")
if call["tool"] == "get_weather":
result = get_weather(**call["args"])
else:
result = f"未知工具:{call['tool']}"
print("\n工具执行结果:")
print(result)
if __name__ == "__main__":
main()
通过parse_tool_call,正则匹配指定字段中的内容,将其解析为json对象,最终在外部判断对象并调用匹配的工具方法
利用LangChain进行API调用
在LangChain中,@tool装饰器能够标准化工具调用的输入和输出信息
当我们使用时,只需要写好注释(让LLM知道工具用途),绑定给上下文即可。无需人为制定工具的发送格式和接受格式,装饰器会自动处理好发送和接收解析。
下面是是利用@tool装饰器来实现工具调用
DEFAULT_USER_PROMPT = "帮我查一下 Beijing 的天气和时间"
SYSTEM_PROMPT = """
你是一个助手。
如果用户询问天气,请调用 get_weather 工具,不要自己编造天气。
如果用户询问时间,请调用 get_time 工具,不要自己编造时间。
""".strip()
@tool
def get_weather(city: str) -> str:
"""Get the weather for a city."""
weather = {
"Beijing": "晴天,25 度",
"Shanghai": "多云,28 度",
"Hangzhou": "小雨,22 度",
}
return f"{city} 的天气是:{weather.get(city, '未知天气')}"
@tool
def get_time(city: str) -> str:
"""Get the current time for a city."""
time = {
"Beijing": "14:00",
"Shanghai": "14:00",
"Hangzhou": "14:00",
}
return f"{city} 的当前时间是:{time.get(city, '未知时间')}"
def load_llm() -> ChatOpenAI:
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
return ChatOpenAI(
model=os.getenv("MODEL", "deepseek-v4-flash"),
base_url=os.getenv("BASE_URL", "https://api.deepseek.com"),
api_key=os.getenv("API_KEY", "sk-1dd85ee7aa0f48fe85399527637d1b9e"),
temperature=0,
)
def main() -> None:
user_prompt = " ".join(sys.argv[1:]).strip() or DEFAULT_USER_PROMPT
print("=== 03. LangChain 原生 ToolCall ===")
print("\n用户请求:")
print(user_prompt)
tools = [get_weather, get_time]
tool_map = {
tool.name: tool for tool in tools
}
llm = load_llm().bind_tools(tools)
messages = [
SystemMessage(content=SYSTEM_PROMPT),
HumanMessage(content=user_prompt),
]
response = llm.invoke(messages)
print("\n模型文本输出:")
print(response.content)
print("\nLangChain 解析出的 tool_calls:")
print(response.tool_calls) # 可能解析出多个工具调用[{},{}]
messages.append(response)
for tool_call in response.tool_calls:
print("\n准备执行工具:")
print(f"tool_name = {tool_call['name']}")
print(f"tool_args = {tool_call['args']}")
toolFunc = tool_map[tool_call['name']]
result = toolFunc.invoke(tool_call["args"])
print("\n工具执行结果:")
print(result)
# 每一个 tool_call 都必须对应一个 ToolMessage
messages.append(
ToolMessage(
content=result,
name=tool_call["name"],
tool_call_id=tool_call["id"],
)
)
if response.tool_calls:
final_response = llm.invoke(messages)
print("\n把工具结果交回模型后的最终回答:")
print(final_response.content)
if __name__ == "__main__":
main()
AI在识别工具调用意图后会产生多个调用记录在response.tool_calls中
[
{'name': 'get_weather', 'args': {'city': 'Beijing'}, 'id': 'call_00_LWmL9pnIhpQKeQ7b1vFj2356', 'type': 'tool_call'},
{'name': 'get_time', 'args': {'city': 'Beijing'}, 'id': 'call_01_y9GlIQ0AgTn1BHWtHDqp3642', 'type': 'tool_call'}
]
其中携带了id,需要在调用完工具后,将调用的结果拼接回历史记录 messages.append(ToolMessage(...))
整个链路流程如下所示
HumanMessage
↓
AIMessage(tool_calls=[天气, 时间])
↓
{ In the Loop # 循环执行工具,并加入到Meesages中
ToolMessage(天气结果)
↓
ToolMessage(时间结果)
}
↓
再次调用 LLM 得到回复
小结
当然LangChain解决的是协议层的问题,在工具注册与调用的过程中,我们并没有直接与模型进行交互,而是通过Json的方式将工具信息发送给API厂商。由厂商来解析工具信息,然后注入上下文。API 厂商处理的是输出时的语法约束和服务端校验(服务层),以API响应的特定格式返回给用户。所以说LangChain封装的是API接口通信的规范,而非模型输入/输出的规范。
说到模型的输入和输出,这就要涉及到上下文工程以及以及模型训练微调相关的知识
模型输出方面,主流厂商一般会采用约束解码(Constrained Decoding) 或 Grammar(语法)约束

评论(0)
暂无评论