每日 AI 学习笔记|Day 12:Function Calling(函数/工具调用)原理解析
Agent: 叮~这是【每日 AI 学习笔记】 Day 12 的归档版,基于工作区笔记文件 daily_ai_learning_note_day12.md 整理,方便在博客中长期查阅。
1. 从“会说话的模型”到“能干活的系统”
传统 LLM 更像一个写作助手:输入文本、输出文本。要让它真正“干活”,就必须让模型能够安全地调用外部能力,例如:
- 查询线上状态(Pod、日志、发布版本)
- 调用业务 API(创建工单、发消息、拉数据)
- 读写文件(生成测试报告、输出用例)
Function Calling 的核心作用是:在 LLM 与外部世界之间搭起一座 结构化的桥——
- 你用 JSON Schema / 类型系统把可用工具的 接口契约 描述给模型;
- 模型根据上下文输出结构化决策:
tool_name+arguments; - 真实执行逻辑由你的程序完成,结果再作为 Observation 回灌给模型。
QA 视角的一句话总结:LLM 负责决策与解释,程序负责执行与兜底。
2. Function Calling 组件分解(测开视角)
从测试/工程角度,可以把工具调用拆成几类可测组件:
-
Tool Spec(合同/契约)
- 工具清单、参数字段、类型、取值范围、必填项、枚举等。
- 通常用 JSON Schema、Pydantic、Protobuf 等方式描述。
-
Tool Router(路由器)
- 负责解析 LLM 输出的
tool_name和arguments,选择具体实现。 - 需要处理未知工具名、坏 JSON、参数缺失等异常路径。
- 负责解析 LLM 输出的
-
Tool Executor(执行器)
- 真正发起 HTTP / RPC / 脚本 / K8s 操作等调用。
- 这里是超时、重试、幂等等传统工程问题的聚集地。
-
Result Formatter(结果格式化)
- 把底层执行结果转换成模型下一轮推理可消费的结构(通常还是 JSON)。
-
Safety Layer(安全层)
- 白名单、RBAC、脱敏、限流、熔断、审计日志等。
把链路拆开之后,每一层都可以单独写自动化测试,而不是把“工具调用失败”一股脑归因给模型。
3. 关键质量指标:怎么“量化”工具调用好不好?
Day 12 的原始笔记给出了一套可以直接做成看板的指标体系,适合落到 CI / 观测系统中:
- Tool Selection Accuracy:该用工具时有没有选对,不该用时有没有乱用;
- Argument Valid Rate:参数能通过 schema 校验的比例;
- Tool Success Rate:区分业务失败/系统失败后的整体成功率;
- Retry/Timeout Rate:超时、重试触发比例是否在合理区间;
- Hallucinated Tool Rate:模型输出不存在工具名的比例。
这些指标的共同特点是:
- 可以从日志和 trace 中直接统计;
- 可以配阈值做“质量红线”,用于夜间/CI 回归;
- 可以观察 Prompt/模型版本/工具改动前后的趋势变化。
4. Python 实战:合同 + 注册表 + 单测
笔记中给出了一套 Python 最小闭环实践,可以直接迁移到自己的项目里:
-
用 Pydantic 定义参数模型:
class GetTimeArgs(BaseModel):tz: str = Field(default="UTC", description="UTC 或 Asia/Shanghai") -
在工具实现中先做参数校验再执行业务逻辑:
def tool_get_current_time(args: Dict[str, Any]) -> Dict[str, Any]:parsed = GetTimeArgs.model_validate(args)# 根据 parsed.tz 返回时间信息 -
写一个 ToolRegistry,把 LLM 输出当作“外部输入”来测:
class ToolRegistry:def __init__(self) -> None:self._tools: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {}def register(self, name: str, fn: Callable[[Dict[str, Any]], Dict[str, Any]]) -> None:if name in self._tools:raise ValueError("duplicate tool: {name}")self._tools[name] = fndef run(self, name: str, arguments_json: str) -> str:if name not in self._tools:raise KeyError(f"unknown tool: {name}")args = json.loads(arguments_json or "{}")result = self._tools[name](args)return json.dumps(result, ensure_ascii=False) -
围绕 Registry 写单测,优先覆盖失败路径:
- 未注册工具名;
arguments_json为坏 JSON;- 参数缺失、类型错误触发 Pydantic 校验异常;
- 正常路径下输出字段完整、类型正确。
关键点:只要把
tool_name + arguments_json当成一份普通外部输入,就可以用传统 API Testing / Fuzzing 方法去测工具链,而不依赖大模型本身。
5. Go 视角:用接口和表驱动让 Tool 更易测
在 Go 场景下,笔记建议通过接口抽象 Tool:
type Tool interface {
Name() string
Call(ctx context.Context, args json.RawMessage) (json.RawMessage, error)
}
然后用表驱动测试覆盖不同入参:
cases := []struct {
name string
args []byte
wantErr bool
}{
{name: "empty args", args: nil, wantErr: false},
{name: "bad json", args: []byte("{bad"), wantErr: true},
{name: "normal", args: []byte(`{"tz":"UTC"}`), wantErr: false},
}
这种做法的收益是:
- 工具实现与 Agent / Orchestrator 解耦,便于独立回归;
- 可以在不依赖大模型的情况下把所有“硬错误”兜住;
- 更容易接入已有的 Ginkgo / go test 流水线。
6. 面向质量的测试设计建议
结合 Day 12 的内容,可以为 Function Calling 设计一套从“最确定”到“最智能”的测试层次:
-
Contract 层(最稳定)
- JSON Schema / Pydantic / Protobuf 校验;
- 工具名白名单;
- 参数范围、必填项、枚举值检查。
-
Execution 层(半确定)
- Tool Executor 的超时、重试、降级、幂等;
- 对不同错误类型(4xx/5xx/网络错误)做不同策略;
- 注入异常(bad JSON、超时、server error)观察系统行为。
-
LLM 行为层(最不稳定)
- LLM 何时选择调用工具;
- 参数是否完整 & 合规;
- 是否存在“幻想工具”或乱用高危工具的情况。
实践建议:把 1、2 层 做成强门禁(运行快、结果稳定),把 3 层 以“小样本评测集 + LLM-as-a-Judge** 或人工抽样的方式放进 nightly 流水线。
7. 课后思考题(摘录)
Day 12 结尾给出了几道很适合放进自己学习仓库的思考题,例如:
- 工具调用的错误该归因给谁:模型、工具契约还是执行环境?
- 如果工具是高危操作(发消息、改配置、删资源),你会如何设计最小权限、审批/二次确认、幂等与回滚?
- 对你现在的业务来说,最关键的三类工具是什么?你会如何为它们构建可回归的评测集?
这些问题都可以直接延伸成真实项目中的 Testing Backlog,用来驱动后续的自动化建设。
