- 给模型一个数据库和三个可以读取它的工具
- 描述这些工具,让模型知道什么时候该用哪一个
- 运行把工具调用转换为工具结果的循环
- 观察它一次请求多个工具
- 把错误交回给模型,而不是直接抛出
- 划清模型”不会做”与”做不到”之间的界线
准备工作
你需要 Python 3.9 或更高版本、requests 包,以及一个 Venice API key。如果你还没有,请参见生成 API Key。其他东西都在标准库里。
agent.py,写入以下 import 和每次调用都会复用的头部代码:
GET /models/traits 会把稳定的 trait 名称映射到当前担任该角色的模型。启动时读取 function_calling_default 意味着当底层模型被替换时,你的智能体依旧能正常工作。完整的 trait 列表请参见 Models。1. 一个值得提问的数据库
任何 SQLite 文件都可以。这里用的是一个小商店,里面有顾客、商品和把它们关联起来的订单,足以让一个真实的问题需要 join 和聚合:2. 模型可以使用的三个工具
这些工具映射了一个人在面对不熟悉的数据库时的做法:先弄清楚里面有什么,再仔细看某张表,然后再查询它。description 不是注释,而是模型在决定该调用哪个工具、参数该填什么时唯一会读到的内容:
3. 循环
函数调用是一段对话,而不是单次请求。模型用工具调用作为回复,你去执行它们,把结果 append 回去,然后再次询问模型。当模型用文本内容而不是工具调用来回复时,循环就结束了。messages。它携带了这些结果所对应的 tool_calls,在推理型模型上它还携带了一个 reasoning_content 字段。手动重新构造这条消息、并丢掉你没预料到的字段,是让第二轮出错的最常见原因。
每个结果通过 tool_call_id 与它对应的调用匹配。除此之外没有其他东西能标识它。
max_rounds 是一个实实在在的上限,不是走个形式。一个一直查询却始终不下结论的模型,若没有这个上限,就会一直循环到你耗尽耐心或耗尽额度为止。
4. 它实际会做什么
接上一个 main 块并运行:stderr,你可以边看边观察它的工作过程:
第 4 轮才是单次函数调用做不到的部分。模型只有在看到上一次的结果之后,才能写出那个查询。
你自己运行时不会与这个逐次调用完全对得上。模型有时会一次描述完三张表,有时会逐张描述,偶尔还会跳过
list_tables 直接猜表名。数据是稳定的,因为它来自数据库;但到达这些数据的路径不是。
第 2 轮在一次响应里返回了三个工具调用,而上面的循环是一个接一个地执行它们。它们彼此独立,所以一旦你的工具真正涉及 I/O,用
ThreadPoolExecutor 就很值得。把 tool 消息保持在与产生它们的调用相同的顺序。usage 数据块能看到它带来的收益:
5. 让错误传达给模型
面对一个失败的查询,直觉是抛出异常。请忍住。错误本身是一种信息,模型可以据此采取行动。 来查询一个不存在的表:run_query 把 {"error": "OperationalError: no such table: purchases"} 作为一个普通的工具结果返回,而不是抛出异常,模型读到了它,调用 list_tables 弄清楚到底有哪些表,然后自己纠正了错误。要是异常向上传播,这个脚本就会因为一个拼写错误直接挂掉。
这就是为什么每个工具在失败路径上也返回 JSON。规则很简单:如果调试你工具的开发者想看到那条错误信息,模型也想看到。
6. 它不会做什么,以及它做不到什么
让智能体去销毁一些东西:SELECT,发现没有结果(因为列里存的是 ES 而不是 Spain),于是就报告了这个:
run_query 里面的护栏才是那个不依赖于”选择”的部分:
上面第二行是
run_query 要在 sqlite3.Error 之外同时捕获 sqlite3.Warning 的原因。Python 的 driver 拒绝堆叠语句,但它是抛出 Warning,而 Warning 并不是 Error 的子类。只捕获 sqlite3.Error 会让堆叠语句逃过处理,直接终止循环,而不是返回一条模型能读到的信息。控制何时调用工具
tool_choice 决定模型有多大的话语权:
"required" 比看起来更”钝”。把 tool_choice 设为 "required",然后问这个智能体 What is 2 + 2?,它会调用 list_tables,看一眼一个对它毫无用处的数据库,然后在下一轮回答 4。而使用 "auto" 时,它会立刻回答 4,什么工具都不调用。只在某个工具确实必须运行的时候才使用 "required"——例如记录一次请求——除此之外都不要动它。
调优智能体
下一步
你现在拥有的这个循环,就是绝大多数智能体背后的那一个。变化的只是工具。- 把 SQL 工具换成 HTTP 调用,它就变成了一个 API 智能体。
- 添加 网络搜索与抓取 作为工具,它就能在回答中途实时查询网络。
- 用结构化响应让它返回有类型的结果,而不是文本。
- 到 Private Research Agent 中查看这个模式的一个更大版本。
函数调用
tools 数组和 tool_choice 的参考。
结构化响应
把最终回答约束到一个 JSON schema。
Prompt 缓存
让不断增长的对话保持低成本。
Private Research Agent
同一个循环,配合网络工具和一个 planner。