有参数验证、可见工具结果及失败上限的 agent 循环。
01/工具调用是请求,并非已执行的行动
聊天模型可提出函数名称及参数;由你的程序决定是否执行,交回结果,再让模型解释。这个循环是小型本地 agent 的实用基础。模型支持工具调用,不代表自动获得文件、浏览器或终端权限。
我们会查询虚构库存。成功的 agent 应查找两项货品,并报告工具返回的数量。这比单看一句“我已检查库存”更有意义,因为你能看到工具真正执行的证据。
02/运行有明确限制的示例
安装 Python 3 及 Ollama,下载 qwen3:4b,并保持 Ollama 运行。下载下方 stock-agent.py。它只使用标准库及程序内的库存字典,不能购物、修改文件或运行 shell。Windows 可按安装方式使用 py stock-agent.py。
程序只允许一种工具、两个商品名称、合共四次调用及四轮模型请求。模型会收到 JSON schema,但 Python 仍会再次验证返回参数。Schema 是给模型的说明,不能代替运行时检查。
ollama pull qwen3:4b
python3 stock-agent.py03/先查执行记录,再查答案
测试数据是 cable: 12、adapter: 0。接受最终解释前,先确认两项商品都有 verified tool-result 记录。文句可不同,但两个数字不能改变。如模型完全没有用工具便作答,程序会刻意报错,不会把它当作已验证查询。
修改其中一个库存数再重跑,之后查询清单以外的商品。模型可能要求澄清,或程序拒绝无效调用;两者都比暗中编造结果好。达到轮数上限的模型,即使输出了很多 token,也未算完成任务。
04/有计划地扩大功能范围
正式业务可把字典换成经过验证的只读库存查询,并由程序控制授权及可读取记录。不要直接把模型字符串当成 SQL、shell 命令或任意网址。对发送、购买或删除等工具,加入人工确认。
网页及文档可能包含误导指令,应视作不可信证据,并在提示以外强制执行工具权限。记录工具名称、验证结果及耗时,同时避免记下机密内容。无论模型在本地或云端,这些控制都适用。
05/测量已完成的任务
记录成功率、工具选择、参数有效性及端到端时间,再测试两个和三个 job 同时调用同一模型。同一对话内的并行工具请求,不等于多个独立 job 共用服务器。
TokFire 的本地 agent 测试数据,为支持的运行环境提供可重复测量。通过本库存练习或该测试,都不代表模型可以安全浏览任何网站或运营整家公司。先扩大测试任务,再扩大权限。
完整示例
下载 Python 示例使用虚构数据的教学程序,须先安装文中所述本地运行环境及模型;模型输出可能不同。这些示例不是已公开的测试成绩。
"""Bounded, read-only tool-calling exercise. Requires Ollama and qwen3:4b."""
import json
from urllib.request import Request, urlopen
INVENTORY = {"cable": 12, "adapter": 0} # Fictional teaching data.
TOOLS = [{"type": "function", "function": {
"name": "stock_count", "description": "Look up stock for one item.",
"parameters": {"type": "object", "properties": {
"item": {"type": "string", "enum": list(INVENTORY)}},
"required": ["item"], "additionalProperties": False}
}}]
def chat(messages):
request = Request("http://127.0.0.1:11434/api/chat", json.dumps({
"model": "qwen3:4b", "stream": False, "think": False,
"messages": messages, "tools": TOOLS,
"options": {"num_ctx": 4096, "num_predict": 256, "temperature": 0}
}).encode(), {"Content-Type": "application/json"})
with urlopen(request, timeout=120) as response:
return json.load(response)["message"]
def dispatch(function):
args = function.get("arguments", {})
if function.get("name") != "stock_count":
raise ValueError("Unknown tool")
if not isinstance(args, dict) or set(args) != {"item"}:
raise ValueError("Invalid arguments")
item = args["item"]
if not isinstance(item, str) or item not in INVENTORY:
raise ValueError("Unknown item")
return {"item": item, "count": INVENTORY[item]}
def run(question):
messages = [{"role": "system", "content": "Use stock_count for stock facts. "
"Do not invent inventory. This tool only reads data."},
{"role": "user", "content": question}]
calls_used = 0
for _ in range(4):
message = chat(messages)
messages.append(message)
calls = message.get("tool_calls", [])
if not calls:
if not calls_used:
raise RuntimeError("No tool used: the task is not verified")
print(message.get("content", ""))
return
if not isinstance(calls, list) or calls_used + len(calls) > 4:
raise RuntimeError("Tool-call limit exceeded")
for call in calls:
result = dispatch(call["function"])
calls_used += 1
print("Verified tool result:", json.dumps(result))
messages.append({"role": "tool", "tool_name": "stock_count",
"content": json.dumps(result)})
raise RuntimeError("Stopped: model did not finish within four rounds")
if __name__ == "__main__":
run("How many cables and adapters are in stock?")