boxmoe_header_banner_img

Hello! 欢迎来到我的博客!

加载中

文章导读

Agent – FunctionCall & ToolCall


avatar
xiaoifei 2026年8月17日 11

参考资料
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)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码