智能体 · 工具

工具调用

模式 → 调用 → 结果

模型
Qwen3-1.7B 推理关闭
工具
count_letter, tokenize
调用格式
<tool_call> JSON </tool_call>
结果
3

全部步骤

  1. 为什么要调用工具

    没有工具时,Qwen3-1.7B 把 “strawberry” 拼出来,花了 117 个词元,仍然答 2;实际是 3 个。它从来看不到字母:“ strawberry” 在它的词表里是单个词元。数数是几行代码的事。

    “ strawberry” = strawberry · 回答 2,实际 3

  2. 把工具写成 JSON schema

    对话模板把每个工具以 JSON schema 的形式写进系统提示词:名字、描述和带类型的参数,后面跟着调用它们的格式。两个工具每次请求要花 241 个词元。

    2 个工具 · 系统提示词 241 个词元

  3. 模型写出一个调用

    模型用一个调用而不是文本来回答:夹在 <tool_call> 标签之间的 JSON。它的第一个词元 <tool_call> 的概率是 100%;在名字处,它把 100% 放在 “count” 上。参数是从问题里复制来的。

    count_letter(word="strawberry", letter="r")

  4. 检查、运行、返回

    程序解析 JSON,对照 schema 检查,运行函数,再把结果作为一个 <tool_response> 写回到新的一轮。模型什么都不运行;它只读取返回的内容。

    检查通过 · 结果 3

  5. 回答,以及何时不调用

    结果进入上下文后,模型回答:“The letter "r" appears 3 times in "strawberry".”。同样的工具并不强制调用:问它 “What is the capital of France?” 时,它给 <tool_call> 的概率 <0.1%,直接作答。

    工具有用就调用,没用就直接回答

  6. 约束解码

    服务器也可以强制调用合法。在名字处,只允许以某个已声明工具名开头的词元:151,669 个中的 8 个。其余的概率为零,就像因果掩码一样,允许的那些再重新归一化。

    名字处允许 151,669 个词元中的 8 个

代码

tools = [{'type': 'function', 'function': {'name': 'count_letter', 'parameters': {...}}}]
text = tokenizer.apply_chat_template(messages, tools=tools, add_generation_prompt=True, enable_thinking=False, tokenize=False)
out = generate(text)                    # '<tool_call>\n{"name": "count_letter", "arguments": {...}}\n</tool_call>'
call = json.loads(re.search(r'<tool_call>(.*?)</tool_call>', out, re.S).group(1))
jsonschema.validate(call['arguments'], schema)   # a malformed call never reaches the function
result = FUNCTIONS[call['name']](**call['arguments'])
messages += [{'role': 'assistant', 'tool_calls': [{'function': call}]}, {'role': 'tool', 'content': str(result)}]

延伸阅读