ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

60 天后端转 Agent【02】:Function Calling 到底是怎么落到代码里的?

60 天后端转 Agent【02】:Function Calling 到底是怎么落到代码里的? 本篇定位这一篇承接上一篇对 LLM API、Messages、Tool Call 和 Agent Loop 的理解不再重复讨论“什么是 Agent”而是把 Tool Call 落地到程序执行层。文章以 Python 为主要示例同时给出 Java / Go 的等价抽象。一、拆解第一篇被折叠的执行链路上一篇我们看到模型可以返回一个工具调用请求。例如{ name: get_weather, arguments: {\city\:\北京\} }这里有一个非常容易忽略的细节在本文采用的 Chat Completions 风格示例中function.arguments 是 JSON 编码后的字符串而不是 Python 字典。程序不能直接把它拿去做关键字参数展开。raw_arguments tool_call.function.arguments args json.loads(raw_arguments)所以从模型输出进入程序之后第一条真正的执行链是Tool Call ↓ arguments 字符串 ↓ json.loads() ↓ Python dict ↓ 参数校验 ↓ 函数调用这一层非常像传统后端接收 HTTP JSON网络层拿到的是序列化数据真正执行业务前仍然需要解析、校验和路由。二、Function Calling 的本质一次动态路由如果把 Function Calling 理解成“大模型直接调用 Python 函数”很容易在后面形成错误的工程思维。更准确的说法是模型生成工具名称和参数宿主程序根据这些数据决定是否执行以及如何执行。对象负责什么Tool Call模型提出的一次结构化调用请求Tool Registry工具名称到 Handler / Function 的受控映射Dispatcher解析、查找、校验、执行并统一处理结果Tool真正访问数据库、API、文件系统或执行计算的业务逻辑LLM ↓ Tool Call ├── Tool Name └── Arguments ↓ Tool Dispatcher ↓ Tool Registry ↓ Handler / Function ↓ Tool Result ↓ LLM因此Function Calling 真正解决的问题不是“让模型执行代码”而是建立一条从“模型生成的结构化意图”到“受控程序执行”的桥梁。三、先把 Python 函数搞懂为什么 **kwargs 能接住模型参数第二篇从函数讲起不是因为 Agent 依赖 Python而是因为 Python 的函数对象和关键字参数机制能够非常直观地展示 Tool Dispatcher 的底层思想。1. 普通函数def get_weather(city: str) - str: return f{city}天气查询结果 result get_weather(北京)2. 位置参数与关键字参数get_weather(北京) get_weather(city北京)Agent 生成的参数天然更接近第二种形式一个“参数名 → 参数值”的映射。3. **kwargs把字典展开成关键字参数args { city: 北京 } get_weather(**args)它可以先理解为get_weather(city北京)所以 Agent 中常见的func(**args)真正表达的是从模型返回的参数字典中取出字段并按照函数的参数名称调用真实函数。4. 函数也是对象def calculator(expr: str) - str: return f计算{expr} f calculator result f(2 3)这里 f 保存的是函数对象本身而不是函数调用结果。正因为函数可以作为值保存我们才能建立 Tool Registry。四、Tool Registry为什么一定要有“工具注册表”假设我们现在有三个工具def get_weather(city: str): ... def calculator(expr: str): ... def search_order(order_id: str): ...模型返回的却只有一个字符串name get_weather程序需要解决的问题是这个字符串应该映射到哪段代码最简单、也最重要的答案就是受控注册表TOOL_MAP { get_weather: get_weather, calculator: calculator, search_order: search_order, }然后func TOOL_MAP.get(name)如果找到func 就是一个可调用对象如果找不到程序应该拒绝执行。为什么不直接根据字符串解释代码因为模型输出本质上是外部输入。Tool Registry 相当于给 Agent 的执行能力建立了一层白名单。模型返回工具名 ↓ 是否存在于受控 Registry ↓ 是 ↓ 找到 Handler ↓ 参数校验 ↓ 执行从更抽象的后端设计来看这与 Handler Mapping、RPC Method Registry、Command Pattern 中的“名称 → 执行器”思想是相通的。五、Tool Dispatcher真正把 Function Calling 接起来有了函数和 Tool Registry下一步就是把模型返回的 Tool Call 变成一次真正的工具调用。这个角色就是 Tool Dispatcher。一个最小版本可以这样写import json def dispatch_tool(name: str, raw_arguments: str, tool_map: dict) - str: func tool_map.get(name) if func is None: return f未知工具{name} try: args json.loads(raw_arguments) except json.JSONDecodeError as exc: return f参数 JSON 错误{exc.msg} try: result func(**args) return str(result) except TypeError as exc: return f参数错误{exc} except Exception as exc: return f工具执行失败{exc}这个版本虽然还不是生产级 Runtime但已经把最重要的职责分离出来查找工具、解析参数、执行函数、处理异常。一次调用逐行拆解假设模型返回name get_weather raw_arguments {city:北京}第一步工具查找。func tool_map.get(name)第二步JSON 反序列化。args json.loads(raw_arguments)第三步关键字参数展开。result func(**args)于是func get_weather args {city: 北京} func(**args) # 等价理解 get_weather(city北京)六、参数校验JSON 合法不代表参数合法这是 Tool Runtime 从 Demo 走向工程代码的第一个关键节点。json.loads() 只能回答“这是不是合法 JSON”不能回答“这些字段是否符合函数和业务的要求”。例如工具def get_weather(city: str): ...模型可能返回{}或者{ city: 北京, foo: bar }还可能返回类型不符合预期的数据。于是应当把流程拆成模型输出 ↓ JSON Parse ↓ 字段检查 ↓ 类型 / 必填项 / 范围 ↓ 权限与业务约束 ↓ Tool Execution注意Python 的类型注解本身不等价于完整的运行时参数校验。inspect.signature读取函数签名import inspect def search_order(order_id: str, limit: int 10): ... signature inspect.signature(search_order) print(signature) (order_id: str, limit: int 10)进一步可以使用 bind() 检查参数能否按照函数签名进行绑定signature.bind(order_id10001, limit5)在生产级 Python Agent Runtime 中如果工具数量增加、参数结构变复杂仅依赖 inspect.signature() 做绑定检查通常还不够。更常见的做法是把“工具参数”提升为明确的数据模型在进入工具执行前使用强类型约束进行自动化类型解析与校验并进一步叠加业务规则校验Python 工程中常见的方案就是 Pydantic BaseModel。例如可以使用 Pydantic 的 BaseModel 定义工具入参from pydantic import BaseModel, Field class WeatherArgs(BaseModel): city: str Field(min_length1) unit: str celsius args WeatherArgs.model_validate({ city: 北京, unit: celsius })这样做的价值在于JSON 解析、字段类型、默认值以及更复杂的字段约束可以从“手写 if/else”中进一步抽离出来。对于 Java / Go 开发者可以把它理解为更接近 DTO Validator 的思路而不是单纯依赖 Python 的动态参数绑定。因此可以把参数校验分成三个层次JSON Parse 负责“能不能解析”函数签名负责“能不能绑定”Pydantic / 业务 Validator 负责“数据是否满足类型与业务约束”。它非常适合帮助我们理解“工具接口定义”与“函数真实签名”之间的关系但它不是完整的业务安全校验。Tool Schema 与 Python FunctionPython Function ↓ 函数名 / 参数 / 类型 / 默认值 ↓ Tool Schema ↓ LLM ↓ Tool Call所以 Tool Schema 本质上可以理解为将程序函数的接口信息转换成模型能够理解的工具描述。七、最容易漏掉的协议细节tool_call_id 与消息顺序当模型发起一次 Tool Call 时调用本身需要有一个标识。例如id: call_123程序执行完工具后需要把结果与这次调用对应起来。在本文采用的 Chat Completions 风格流程中可以用 tool_call_id 关联assistant message └── tool_calls └── id call_123 ↓ tool message └── tool_call_id call_123消息历史应体现出“模型先提出调用、程序再返回结果”的顺序而不是只把一个孤立的 Tool Result 塞回去。完整的消息关系是User ↓ Assistant tool_calls ↓ Tool tool_call_id ↓ Assistant / Final Answer这个细节之所以重要是因为 Tool Result 必须能在上下文中明确对应到哪一次模型调用。多工具调用场景下这一点尤其重要。八、异常处理Tool 出错不等于 Agent 必须崩溃Agent Runtime 需要把“模型失败”和“工具失败”区分开。比如数据库超时不代表模型能力有问题一个未知工具也不代表整个用户请求必须直接让进程崩溃。失败位置示例典型处理Tool Lookup未知工具拒绝执行并记录JSON Parse非法 JSON返回明确解析错误Parameter Validation缺少必填参数不执行工具返回参数错误Tool ExecutionDB / RPC 超时按业务决定是否有限重试Permission无权执行直接拒绝Side Effect已下单 / 已扣款确认幂等性后再决定是否重试“把异常 catch 住”不是完整的异常处理。真正的工程问题是这个错误应该让谁知道是否可以重试是否需要降级是否已经产生副作用九、Java / Go / Python语言不同Tool Runtime 抽象不变如果你主要使用 Java 或 Go不需要把这篇文章当成 Python 入门课。重点是理解统一的抽象Tool Name ↓ Registry ↓ Handler / Function ↓ Arguments ↓ Execute ↓ ResultPythonTOOL_MAP { get_weather: get_weather, } func TOOL_MAP[get_weather] result func(city北京)JavaMapString, ToolHandler tools Map.of( get_weather, this::getWeather ); ToolHandler handler tools.get(get_weather); Object result handler.execute(args);Gotools : map[string]func(string) string{ get_weather: getWeather, } handler : tools[get_weather] result : handler(北京)你会发现三种语言最终都在解决同一个问题把“模型返回的字符串工具名”映射成一个受控的执行器。十、实战手写一个 Mini Tool Runtime这一篇不再直接上 LangChain。先用最少的代码自己把 Tool Runtime 跑通。项目结构mini-tool-runtime/ ├── main.py ├── tools.py ├── registry.py ├── dispatcher.py └── tests.py建议按四个 Level 完成。Level 1函数调用def get_weather(city: str) - str: return f{city}今天晴天 get_weather(北京)完成标准函数定义正确、参数能够传入、返回值能够拿到。常见坑把“函数对象”和“函数调用结果”混在一起把类型注解误认为运行时校验。Level 2Tool RegistryTOOL_MAP { get_weather: get_weather, } func TOOL_MAP[get_weather]完成标准能够从工具名称找到函数对象并解释 Registry 为什么是受控白名单。常见坑使用模型输出直接拼接代码或解释执行工具名与 Registry 中的名称不一致。Level 3JSON 参数解析与 Dispatchername get_weather arguments {city:北京} result dispatch_tool( name, arguments, TOOL_MAP )完成标准至少覆盖正常工具、未知工具、非法 JSON、缺失参数、多余参数、工具内部异常。常见坑把 JSON 字符串当 dict所有异常都被统一转成“失败”未知工具仍继续执行。Level 4接入真实 LLMUser ↓ LLM ↓ Tool Call ↓ Dispatcher ↓ Tool ↓ Tool Result ↓ LLM上一篇60 天后端转 Agent01别一上来就 LangChain先看懂一次真实的 Tool Call-CSDN博客系列【2026】后端开发怎么转 Agent 60天从入门到精通全网最详细收藏这篇就够了-CSDN博客
返回列表