你会创建或学到什么

有参数验证、可见工具结果及失败上限的 agent 循环。

01/工具调用是请求,并非已执行的行动

聊天模型可提出函数名称及参数;由你的程序决定是否执行,交回结果,再让模型解释。这个循环是小型本地 agent 的实用基础。模型支持工具调用,不代表自动获得文件、浏览器或终端权限。

我们会查询虚构库存。成功的 agent 应查找两项货品,并报告工具返回的数量。这比单看一句“我已检查库存”更有意义,因为你能看到工具真正执行的证据。

资料来源Ollama · Tool calling

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.py
资料来源Ollama · Qwen3 4B modelOllama · Tool calling

03/先查执行记录,再查答案

测试数据是 cable: 12、adapter: 0。接受最终解释前,先确认两项商品都有 verified tool-result 记录。文句可不同,但两个数字不能改变。如模型完全没有用工具便作答,程序会刻意报错,不会把它当作已验证查询。

修改其中一个库存数再重跑,之后查询清单以外的商品。模型可能要求澄清,或程序拒绝无效调用;两者都比暗中编造结果好。达到轮数上限的模型,即使输出了很多 token,也未算完成任务。

04/有计划地扩大功能范围

正式业务可把字典换成经过验证的只读库存查询,并由程序控制授权及可读取记录。不要直接把模型字符串当成 SQL、shell 命令或任意网址。对发送、购买或删除等工具,加入人工确认。

网页及文档可能包含误导指令,应视作不可信证据,并在提示以外强制执行工具权限。记录工具名称、验证结果及耗时,同时避免记下机密内容。无论模型在本地或云端,这些控制都适用。

05/测量已完成的任务

记录成功率、工具选择、参数有效性及端到端时间,再测试两个和三个 job 同时调用同一模型。同一对话内的并行工具请求,不等于多个独立 job 共用服务器。

TokFire 的本地 agent 测试数据,为支持的运行环境提供可重复测量。通过本库存练习或该测试,都不代表模型可以安全浏览任何网站或运营整家公司。先扩大测试任务,再扩大权限。

使用虚构数据的教学程序,须先安装文中所述本地运行环境及模型;模型输出可能不同。这些示例不是已公开的测试成绩。

"""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?")