系列:100 天系统学习 AI Agent 开发
当前阶段:RAG、知识库与工具边界
今日目标:FunctionAgent 适合从简单函数工具开始,把确定性能力交给代码,把选择权交给模型。

第一个工具越普通,越容易看清边界

如果一上来就给 Agent 搜索、数据库、文件写入十几个工具,出错时很难知道是工具描述、参数、路由还是权限问题。今天我只设计一个 get_learning_plan(day):输入天数,返回对应学习任务。它看起来不起眼,正好能把“模型选工具”和“代码执行规则”分开。

FunctionAgent 的核心并不是函数前面加一个装饰器,而是让模型在合适的时候选择一个有明确契约的函数。模型可以理解“我学到第 18 天了,接下来做什么”;函数负责检查 day 是否有效、从可信数据读取任务并返回稳定结构。

先把普通函数写好

from dataclasses import dataclass

@dataclass(frozen=True)
class LearningPlan:
    day: int
    topic: str
    task: str
    source: str

PLANS = {
    17: LearningPlan(
        day=17,
        topic="LlamaIndex 入门",
        task="整理 Agent、Tools、Workflows 的责任边界",
        source="roadmap.md",
    ),
    18: LearningPlan(
        day=18,
        topic="FunctionAgent 初体验",
        task="设计并验证 get_learning_plan(day)",
        source="roadmap.md",
    ),
}

def get_learning_plan(day: int) -> LearningPlan:
    """返回指定天数的学习计划;仅接受已收录的 1-100 整数。"""
    if isinstance(day, bool) or not isinstance(day, int):
        raise TypeError("day 必须是整数")
    if not 1 <= day <= 100:
        raise ValueError("day 必须在 1 到 100 之间")
    if day not in PLANS:
        raise LookupError("该天计划尚未载入,请先读取路线文件")
    return PLANS[day]

这段函数可以脱离模型单独测试。示例字典只放了两天,因此查询 Day 19 预期得到“尚未载入”,而不是编造计划。真正项目应从受控的 roadmap.md 解析或从数据库读取。

工具契约比函数名字更重要

项目 设计
名称 get_learning_plan
使用时机 用户询问某一天的主题或任务
输入 day: 1–100 的整数
输出 day、topic、task、source
可预期错误 类型错误、范围错误、计划未载入
权限 只读
不负责 修改进度、生成整套课程、写文件

如果把它命名为 get_info(day),模型很难判断“info”是什么;如果描述只写“获取数据”,调用质量也不会稳定。好工具描述应同时说明做什么、什么时候用、不能做什么。

Agent 应该只拥有有限选择

成功

参数缺失

数据未载入

用户输入

是否在询问具体学习日?

直接回答或追问

提取并校验 day

调用 get_learning_plan

解释结构化结果并标来源

请用户补充天数

承认暂无资料,不编造

接入 LlamaIndex FunctionAgent 时,具体导入路径、参数名和运行方法要以当前官方文档为准。下面只写框架无关伪代码,不能当作已验证 API:

agent = FunctionAgent(
    tools=[get_learning_plan],
    instructions="只有询问具体天数时才调用工具;没有资料就明确说明"
)
result = await agent.run(user_message)

最小测试不需要模型

def test_get_learning_plan():
    assert get_learning_plan(18).source == "roadmap.md"

    for bad_day in (0, 101, "18", True):
        try:
            get_learning_plan(bad_day)
        except (TypeError, ValueError):
            pass
        else:
            raise AssertionError(f"无效参数未被拒绝:{bad_day!r}")

还要给 Agent 层准备路由样本:

  • “第 18 天学什么?”应调用一次;
  • “你好”不应调用;
  • “明天学什么?”若不知道当前 day,应追问;
  • “把第 18 天改成数据库课”不应调用这个只读工具;
  • “第 999 天”应在工具前或工具内被拒绝。

今天真正学到的不是 FunctionAgent 语法

确定性能力先写成可独立测试的普通函数,再让模型拥有有限选择权,这个顺序很重要。函数错误不该由模型悄悄改写,模型误调用也不能绕过函数校验。下一篇把这个工具放进知识库助手时,还要加检索证据、引用和无法回答的分支。

面试官会追问:FunctionAgent 调工具时,谁对结果负责?

模型只负责“建议调用”;宿主程序负责参数校验、鉴权、执行、超时、重试和审计。工具返回也不能直接当真:需要区分 success、retryable_error、user_action_required 与 fatal_error,并把给模型看的摘要和内部诊断分开。

我会用三条 trace 证明边界:

  • 参数缺失:schema 在执行前拒绝,并允许模型修复一次。
  • 临时超时:只对只读或带幂等键的动作重试。
  • 写操作结果未知:先查询业务状态,绝不盲目重放。

如果面试官追问“工具很多怎么办”,答案不是全部塞进上下文,而是先按场景检索或路由出小规模 tool loadout,减少误选与 Token 消耗。

今日检查清单

  • 普通函数不依赖模型也能测试
  • 工具名、参数、返回值和错误契约具体
  • 只读与写操作没有混在一个函数里
  • Agent 层有“何时不调用”的样本
  • SDK 伪代码明确标注,编码时核对官方版本
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐