Skip to content
This page has been auto-translated and may contain errors.View in English

工具使用

docs.scrimba.com

工具使用(通常称为函数调用)是语言模型突破自身文本框的方式:你给它一个可能会请求运行的函数列表,它决定何时使用它们。模型本身永远不会运行任何东西,它只会请求调用,而你的代码才是做实际工作的。

模型本身被封闭在一个盒子里。它无法检查今天的天气、查找数据库中的用户,或发送电子邮件。它只是根据训练时学到的内容预测文本,这是大语言模型如何工作中相同的静态画面。工具使用就是让它超越盒子的方式,也是智能体构建的基础。

要记住的关键词是"请求"。模型告诉你它想要哪个函数以及什么参数。你的代码运行它并返回结果。你始终保持控制权。

模型只能生成文本,所以它无法获取实时数据、接触数据库或触发操作。工具使用弥补了这一差距:你描述一组函数,模型发出一个结构化请求来调用其中一个,你的代码运行它,然后你把结果反馈回来。模型协调,你的代码执行。

本章是你将构建的每个智能体的底层机制。在这里把循环、工具架构和错误路径理解正确了,智能体就变成了这个模式的重复应用。

工具使用是概率文本生成器与确定性代码的接缝,大部分生产环境的痛点都存在于这条接缝上。模型预测一个结构化调用,你的代码执行它,结果被反馈作为更多上下文。使这一切困难的所有东西(验证不受信任的参数、幂等性、额外的往返、幻觉调用、可观测性)都源于这一个握手。

这里没有新的数学。这是关于让一个随机系统在你的系统中触发真实副作用的工程纪律,也是整个智能体章节所基于的基础。

循环

工具使用是一个**往返交互**,而不是单一调用:

  1. 你发送用户的消息加上一个工具列表,模型可以使用这些工具。
  2. 模型以两种方式之一回复:一个正常的答案,或一个工具调用请求,附带它想要的参数。
  3. 如果它请求了工具,你的代码运行那个函数并把结果发送回模型。
  4. 模型使用结果来编写它的最终答案。

这就是全部模式。模型请求,你的代码运行。第 2 和 3 步可以重复,如果模型需要多个工具,这就是几章后智能体的雏形。

Juno循环 工具使用是一个循环:你发送一条消息加上工具列表,模型要么回答要么请求用参数调用工具,你的代码运行函数,你把结果发送回来供模型使用。模型本身不运行代码,它只请求调用,所以你保持控制权。重复中间的步骤,你就有了智能体的雏形。

循环有四个节拍:发送消息加工具列表,模型回答或请求调用,你的代码运行函数,结果返回,模型继续。初学者常常遗漏的部分是这很少是一个往返。模型可以请求一个工具,读取你的结果,然后根据所见请求另一个工具,依此类推。

所以真正的形状是一个**多轮循环**:持续调用模型并运行它请求的任何工具,直到返回一个不包含工具调用的响应。那个最终无工具的响应就是给用户的答案。

python
messages = [{"role": "user", "content": user_text}]

while True:
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)              # 记录模型说了什么

    if not msg.tool_calls:            # 没有工具被请求:这就是答案
        return msg.content

    for call in msg.tool_calls:       # 运行每个被请求的调用,不只是第一个
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

循环条件是安全问题。一个不断请求工具的模型永远不会自己退出,所以在生产环境中限制迭代次数,如果达到限制则停止并返回明确的错误。当模型返回不含工具调用的响应时循环结束。

Juno循环 循环是发送工具、模型回答或请求调用、你运行它、你把结果反馈回来、重复。关键是:它持续进行直到模型返回一个不含工具调用的响应,所以把它写成一个真正的循环,而不是一个 if 语句。并且限制迭代,因为一个持续请求工具的模型会乐意在你的账单上永远旋转。

循环在机制上很小:用工具调用模型,运行它请求的任何东西,追加结果,重复直到它返回一个不含工具调用的响应。有趣的部分是每次迭代都是一个完整的模型**往返**,往返是你的延迟和令牌预算花费的地方。

每轮都重新发送整个不断增长的消息历史,所以需要五个工具调用的任务要为提示付费五次,加上随着结果累积输入每轮都在增长。杠杆:

  • 最小化往返次数,通过给模型提供每次调用能做更多事的工具。
  • 保持工具结果紧凑,这样重新发送的历史不会爆炸。
  • 并发运行独立的工具调用而不是序列执行,这样实际延迟跟踪最慢的调用,而不是它们的总和。
python
MAX_TURNS = 8

for turn in range(MAX_TURNS):
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        return msg.content

    # 模型可以在一个轮次中请求多个调用;运行它们,然后继续
    for call in msg.tool_calls:
        messages.append(run_tool(call))   # 已验证、已记录、已封装错误

raise RuntimeError("tool loop did not converge within MAX_TURNS")

硬上限不是可选的。没有它,一个困惑的模型可以无限循环,失败模式是一个缓慢、昂贵的请求,而不是干净的错误。限制它,当你达到限制时,大声失败并记录跟踪,这样你可以看到模型在追踪什么。

Juno循环 循环很简单但运行起来很贵:每轮都是一个通过整个重新发送历史的完整模型调用,所以五个工具调用意味着为提示付费五次。保持结果紧凑,并发运行独立调用,更倾向于较少但功能更丰富的工具而不是许多繁琐的工具。并且硬上限轮次,因为当模型决定永远循环的那一天,你想要日志中的干净错误,而不是五位数的账单。

真正发生了什么

工具使用可能感觉像模型获得了新的力量,但它没有。在底层,模型做它总是做的一件事:预测文本。它生活的盒子没有任何改变。

当你在请求中包含工具定义时,你是在把它们添加到模型预测的上下文中。模型在训练时接触了例子,当给定这样的工具时,正确的延续有时是一个特殊格式的消息,意思是"调用 get_weather,参数为 Beijing",而不是一个普通的句子。所以当你的问题使一个工具看起来有用时,最可能的下一个输出是那个结构化的**工具调用消息**,API 把它以 tool_calls 的形式呈现给你。

模型没有伸出去运行任何东西。它预测了工具调用是正确的举动,并以你的代码知道如何读取的格式写了一个请求。

然后是你,不是模型,运行函数。你取得真实结果并把它放回消息中作为更多上下文,模型从那个扩大的上下文预测它的最终答案。整个功能是两个普通的预测,你的代码在中间做真实的工作。

你是模型的文本框和真实世界之间的桥梁。持有这个画面,本章的其余部分,包括智能体,停止感觉神秘:AI 采取的每个行动都是一个预测的请求,你的代码选择执行。

Juno真正发生了什么 工具使用仍然是纯预测:模型的训练使得,给定其上下文中的工具定义,可能的输出有时是一个结构化的"用这些参数调用这个函数"消息,API 以 tool_calls 的形式交给你。模型不运行任何东西,它预测一个请求。你的代码运行函数并把结果作为更多上下文反馈给下一个预测,所以你是模型和真实世界之间的桥梁。

工具使用不是对模型的新能力的拼凑,它是来自大语言模型如何工作的相同逐令牌预测,针对结构化目标。工具定义进入上下文,模型的训练使得,当工具合适时,最高概率的延续是一个格式化的调用而不是散文。API 解析那个延续并以 tool_calls 的形式交给你。

有用的后果:工具调用是**受约束的生成**,模型产生的输出与你提供的架构相匹配。这正是结构化输出的相同机制,你要求模型返回 JSON 的特定形状。

它们之间的界线是意图。结构化输出是"给我这个形状的数据然后停止"。工具使用是"给我这个形状的数据,这样我可以运行某些东西并反馈结果"。

如果你只需要从模型获得一个解析的对象,使用结构化输出。当模型需要行动然后对你的代码返回的内容做出反应时,使用工具使用。

因为调用是预测的,不是执行的,你的代码是唯一真正接触你的系统的东西。模型提议,你的代码处置,那个边界是你在任何真实事物发生之前放置每个检查的地方。

Juno真正发生了什么 工具调用是相同的逐令牌预测,针对架构而不是散文:API 以 tool_calls 的形式呈现那个预测。它是受约束的生成,与结构化输出相同的机制。区别在于意图:结构化输出给你数据然后停止,工具使用给你数据,这样你可以运行某些东西并做出反应。无论哪种方式,模型只是提议,你的代码是唯一接触任何真实东西的东西。

工具调用是预测的文本,不是执行,保持区分清楚是什么保护了你。模型发出给定上下文中工具的最高概率延续,API 把它解码为结构化调用,你的代码决定是否执行它。在机制上它是结构化输出附加一个副作用。

那个框架精确地设置了信任边界。工具调用中的参数是模型输出,这意味着它们是**不受信任的输入**:一个概率系统生成的文本,与任何用户在表单中输入的内容具有相同的地位。这样对待它们。

模型也可以被它在循环中期读到的内容引导,所以如果工具返回来自网页或文档的文本,埋藏在该文本中的提示注入可以劝说模型请求一个不同的工具,参数由攻击者选择。防御是结构性的,不是系统提示中的礼貌指令:验证参数对照严格架构,将每个工具的范围限制在它需要的最窄权限,永远不要让工具的权限超过你会授予可以影响其输入的最不受信任的一方。

所以心智模型是一个请求路由器,不是同事。模型用提议的参数路由到一个函数。你的代码认证、授权、验证,只有那样才执行。这个章节付出代价的地方是"只有那样"下游的一切。

Juno真正发生了什么 工具调用是 API 为你解码的预测文本,附加副作用的结构化输出,永远不是模型执行任何东西。所以参数是不受信任的输入,与陌生人填入的表单字段具有相同的地位,埋藏在工具返回的文本中的提示注入可以重定向下一个调用。验证、限制范围和授权在边界处。模型路由请求,你的代码是唯一在杠杆上的东西。

定义工具

你向模型描述每个工具:它的名称、它做什么,以及它接受的参数。描述不是给你的文档,它是给模型关于何时使用工具的指令,所以像写提示那样写它。

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取城市的当前天气。在用户询问天气时使用。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名,如 '北京'"},
                },
                "required": ["city"],
            },
        },
    },
]

parameters 块是一个**架构**,与结构化输出的概念相同:它定义了模型必须产生的参数。当模型决定调用 get_weather 时,它发送回一个与这个形状匹配的 city 参数。模糊的描述("获取天气")导致模型在错误的时刻使用工具。清楚的描述("在用户询问天气时使用")可以很好地引导它。

Juno定义工具 工具定义有名称、描述和参数的 parameters 架构。描述真的是一个提示:它告诉模型何时使用工具,所以要小心编写。参数架构是与结构化输出相同的机制,限制模型发送的参数。

工具定义有三个部分模型读取:名称、描述和 parameters 架构。描述是人们低估的部分。它不是给你的队友的注释,它是模型用来决定何时调用工具的**提示**,所以像写一个那样写它:说工具做什么,何时使用它,何时不使用。

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "按账户电子邮件查找客户的订单。"
                "仅在用户询问现有订单时使用。"
                "不要用于产品问题或退款。"
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "要搜索的账户电子邮件"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "可选状态过滤器",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

收紧架构,你就收紧了模型:

  • required 强制工具无法运行就不能少的参数。
  • enum 将字段限制为固定集合,所以模型无法发明第五个状态。
  • additionalProperties: false 拒绝你没有定义的字段。

架构是你的第一道防线:它越窄,模型调用工具错误的方式就越少。

这个工具形状是 OpenAI 风格的;确切的键因提供商而异(Anthropic 和其他人以不同方式嵌套架构),但三个部分,名称、描述、架构,在任何地方都相同。

Juno定义工具 名称、描述和 parameters 架构,那就是工具。描述是一个提示,不是文档字符串:告诉模型何时调用它,何时不调用,否则它会在错误的时刻调用它。用 requiredenum 和没有额外属性来收紧架构,因为窄架构是模型调用工具错误的更少方式。确切的 JSON 键因提供商而异,但那三个部分不会。

工具定义是两个戴着一顶帽子的提示。描述控制模型何时调用工具,架构限制它可能发送什么参数,两者都是面向模型的指令,不是内部文档。架构是你可以严格的地方,严格在这里是你拥有的最便宜的幻觉控制。

python
{
    "name": "issue_refund",
    "description": (
        "针对特定订单行发放退款。"
        "仅在确认订单 ID 和金额与用户后使用。"
        "永远不要为超过订单总额的金额调用。"
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "pattern": "^ord_[0-9]{8}$"},
            "amount_cents": {"type": "integer", "minimum": 1, "maximum": 100000},
            "reason": {"type": "string", "enum": ["damaged", "wrong_item", "late"]},
        },
        "required": ["order_id", "amount_cents", "reason"],
        "additionalProperties": False,
    },
}

一个**受约束的字段**(一个有 enumpatternminimum/maximumrequired 设置的字段)缩小了模型可以发出的内容,但清楚地读取限制:许多提供商验证架构的结构,但值仍然来自预测,所以一个紧架构减少了格式错误的调用,而不是证明调用是正确或安全的。order_id 匹配一个模式并不意味着那个订单存在或属于这个用户。所以架构是一个过滤器,不是授权检查。

你仍然根据真实状态验证每个参数,订单存在、金额不超过总额、调用者拥有它,在副作用运行之前在你的代码中。架构验证捕获错误的形状;只有你的代码捕获错误的操作。(架构键和每个提供商强制的严格程度因提供商而异,所以确认你的提供商的行为而不是假设约束被强制。)

还有一个杠杆:工具数量。超过大约一打工具,选择精度下降,模型开始为错误的工具而求助,每个定义也坐在上下文中在每次调用时消耗令牌。保持活跃工具集小且与任务相关,而不是一次暴露你的整个 API 表面。

Juno定义工具 描述控制何时调用,架构控制可以发送什么,两者都是提示。用 enumpattern 和界限锁定架构,但不要把一个有效的形状误认为是一个有效的操作:一个格式良好的 order_id 仍然是不受信任的,所以在副作用之前根据真实状态重新验证。并保持工具集小,因为超过一打模型选择错误,每个定义都在消耗你的上下文。架构过滤,你的代码授权。

处理调用

当模型想要工具时,回复包含 tool_calls 而不是最终答案。你读取请求的函数和参数,运行真实函数,并把结果作为 tool 消息发送回来。然后你再次调用模型,这样它可以完成。

python
import json

# 工具的真实实现
def get_weather(city):
    # 在真实应用中这会调用天气 API;这里我们伪造它
    return {"city": city, "tempC": 18, "condition": "cloudy"}

messages = [{"role": "user", "content": "北京的天气怎么样?"}]

# 1. 首次调用:提供工具
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None

if tool_call:
    # 2. 用模型的参数运行请求的函数
    args = json.loads(tool_call.function.arguments)
    result = get_weather(args["city"])

    # 3. 把模型的请求和你的结果发送回去
    messages.append(response.choices[0].message)         # 助手的工具请求
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

    # 4. 再次调用,这样模型可以使用结果来回答
    response = client.chat.completions.create(model=MODEL, messages=messages)

print(response.choices[0].message.content)
# "北京目前 18 度,多云。"

参数以**JSON 字符串**到达,所以你 json.loads 它们成为真实对象,然后在使用前检查。你把两条消息推送到历史中:模型的工具请求和你的 tool 结果,由 tool_call_id 链接。最终调用把原始天气数据变成自然句子。

Juno处理调用 当模型想要工具时,它的回复带着 tool_calls 而不是文本。你从 JSON 字符串中解析参数,运行真实函数,并推送两条消息回来:模型的请求和你的结果,由 tool_call_id 链接。最终的模型调用把你的原始结果变成自然答案。

当模型想要工具时,它的回复带着 tool_calls 而不是 content。每个调用给你一个函数名、一个 id 和以 JSON 字符串形式的参数。你解析、运行并追加一个 tool 消息,用匹配的 tool_call_id 标记,这样模型知道哪个结果答复哪个请求。

值得正确处理的部分是失败。你的工具会抛出:一个坏参数、一个 404、一个超时。本能是让异常冒泡,但那样会结束整个轮次。更好的做法是**把错误作为数据返回**:捕获错误并把它作为工具结果返回,这样模型可以读取出了什么问题并恢复、用更正的参数重试,或告诉用户它无法做这件事。

python
def run_tool(call):
    try:
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        content = json.dumps(result)
    except Exception as err:
        # 把失败作为数据返回,不是异常
        content = json.dumps({"error": str(err)})
    return {"role": "tool", "tool_call_id": call.id, "content": content}

把错误作为工具消息返回是什么让工具循环有韧性而不是脆弱。一个用格式错误的电子邮件调用 search_orders 的模型得到 {"error": "invalid email"} 并可以要求用户纠正它,而不是你的整个请求 500-ing。响应形状(tool_callstool_call_idtool 角色)是 OpenAI 风格的,因提供商而异,但模式,解析、运行、返回结果或错误,在所有提供商中相同。

Juno处理调用 回复带着 tool_calls:解析 JSON 参数,运行函数,追加一条带匹配 tool_call_idtool 消息。这是关键举措,针对失败:不要让异常冒泡并结束轮次,捕获它并把 {"error": ...} 作为工具结果返回,这样模型可以恢复或重试。确切的字段名因提供商而异,但解析、运行、返回结果或错误在各处相同。

解析和分派调用是常规部分。危险的部分是接收参数和运行副作用之间的一切,因为参数是模型输出,函数做真实的东西。

三个习惯将演示与可以在生产环境运行的工具分开:

  • 首先,在执行前验证,根据真实状态,不是架构:架构已经通过或你不会在这里,所以现在确认订单存在、用户拥有它、金额在范围内。
  • 其次,为**幂等性**构建,即运行相同操作两次的效果与运行一次相同的属性。模型重试、循环重新触发、网络复制,所以写入工具需要幂等性键(通常从 tool_call_id 派生),这样重复不会重复计费或重复发送。
  • 第三,把错误作为数据返回,结构化足以让模型对其采取行动,这样可恢复的失败变成重试,不可恢复的变成给用户的干净消息。
python
def run_tool(call):
    name, args = call.function.name, json.loads(call.function.arguments)
    log.info("tool_call", name=name, args=redact(args), call_id=call.id)  # 可观测性

    try:
        validate(name, args)                       # 真实状态检查,在坏输入时提高
        result = TOOLS[name](args, idem_key=call.id)  # 重试时幂等
        content = json.dumps(result)
    except ValidationError as err:
        content = json.dumps({"error": "invalid", "detail": str(err)})  # 模型可以重试
    except Exception as err:
        log.exception("tool_failed", call_id=call.id)
        content = json.dumps({"error": "tool_failed"})  # 不要泄露内部信息给模型

    return {"role": "tool", "tool_call_id": call.id, "content": content}

两个更多的生产现实:

  • 可观测性:记录哪个工具运行了、什么参数(编辑秘密)和返回了什么,因为当智能体行为不当时工具调用的跟踪是重建它实际做了什么的唯一方式。
  • 破坏性工具:对于任何不可逆的,删除、付款、电子邮件,不要让模型的调用是最终权限。通过确认、一个报告会发生什么的试运行或人工批准步骤把它封闭,并限制凭证范围,这样即使模型错了或被劫持,爆炸半径也是有界的。

架构有效的调用在你的代码授权它之前仍然是不受信任的。(字段名和重试封装是 OpenAI 风格的;纪律跟随任何提供商。)

Juno处理调用 解析是常规部分。在参数和副作用之间:验证根据真实状态而不仅仅是架构,使写入幂等关闭 tool_call_id,因为重试会触发两次,并把错误作为数据返回,模型可以作用于它而不泄露你的内部。记录每个调用及其参数,这样你可以重建智能体实际做了什么。对任何破坏性的东西封闭在确认或有范围的凭证后面,因为架构有效的退款仍然是陌生人的请求,直到你的代码说是。

实践中

相同的流程作为可重用函数。注意,模型和你的系统之间的唯一东西是你写和控制的代码:

python
import json

tool_impls = {"get_weather": lambda args: get_weather(args["city"])}

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    calls = response.choices[0].message.tool_calls
    if not calls:
        return response.choices[0].message.content  # 模型直接回答

    call = calls[0]
    args = json.loads(call.function.arguments)
    result = tool_impls[call.function.name](args)

    messages.append(response.choices[0].message)
    messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

    response = client.chat.completions.create(model=MODEL, messages=messages)
    return response.choices[0].message.content

这处理一个工具调用。模型也可以一次请求多个,叫做**平行工具调用**,你可以通过遍历 tool_calls 中的每个条目来处理。如果你让模型在一个循环中持续调用工具直到完成,每次决定自己的下一步,你就得到了一个智能体,这正是下一章走的地方。

Juno实践中 包装在函数中,工具使用是一个模型调用来选择工具,你的代码运行它,以及第二个调用把结果变成答案。模型和你的系统之间的唯一东西是你写的代码,所以你保持控制权。循环这个直到模型完成,你就有了智能体。

在一个真实的**工具处理器**中,你把多轮循环、平行调用和错误返回组合成一个函数。模型可能在单个轮次中请求多个工具,循环运行直到它停止请求。

python
import json

MAX_TURNS = 6

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for _ in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:        # 处理所有平行调用
            messages.append(run_tool(call))  # 解析、运行、返回结果或错误

    return "抱歉,我无法完成那件事。"  # 达到轮次上限

三个决定使这个生产就绪而不是演示。循环遍历 alltool_calls,不是 [0],否则你会无声地丢弃模型要求的调用。限制 MAX_TURNS 这样模型不能永远旋转。并通过 run_tool 路由失败作为工具消息,这样单个坏调用不会破坏整个交换。

当你停止硬编码步骤并让模型每轮决定自己的下一步时,这个确切的循环变成一个智能体。区别是自主性,不是架构。

Juno实践中 生产处理器是带三件事连线的多轮循环:遍历 tool_calls 中的每个条目而不仅是第一个,限制轮次这样它不能永远旋转,把失败作为工具消息返回这样一个坏调用不会破坏运行。相同的循环,更多自主性,你就有了智能体。架构不变,模型只是得到决定自己下一步。

生产处理器折叠了上面的一切:有界的多轮循环、平行调用并发运行、验证和记录的执行以及作为数据返回的错误。剩下的问题是大多数团队跳过的:何时根本不给模型工具

工具是正确答案,当操作真的取决于模型对自然语言的判断时:用户意味着哪个订单、这算不算退款情况、搜索什么。当路径**确定的**时它是错误答案,相同的输入总是产生相同的调用。如果请求总是触发相同的调用相同的可派生参数,在代码中连线它并跳过往返;你移除一个幻觉表面、一个延迟跳跃和一个令牌成本而没有失去什么。给模型工具买灵活性并以非确定性为代价,所以只在灵活性是重点的地方花费它。

python
def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for turn in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools, tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return msg.content

        results = run_tools_concurrently(msg.tool_calls)  # 独立调用并行
        messages.extend(results)

    log.warning("tool_loop_unconverged", turns=MAX_TURNS)
    return fallback_answer()

两个杠杆针对当你确实使用工具时:

  • 约束工具调用幻觉:模型可以发明一个你从未定义的工具调用或伪造参数,所以彻底拒绝未知工具名,并使用 tool_choice(强制、禁止或释放工具使用的参数)在你知道需要一个工具时要求一个或在你知道没有应该触发时禁止它们,而不是把每个轮次留给机会。
  • 保持可观测性跟踪,工具、参数、结果、轮次计数,因为自主循环只有在你能重放它实际做了什么时才可调试。

这个处理器是智能体的字面种子:智能体是这个循环有更广的工具集和朝向目标链接调用的自由度,这也是为什么失败模式在那里扩大。(SDK 表面是 OpenAI 风格的;tool_choice 和消息封装因提供商而异。)

Juno实践中 完整处理器是带并发、验证、记录工具运行和错误作为数据的有界循环。被跳过的问题是何时不给工具:如果调用是确定的,在代码中连线它并丢弃往返、延迟和幻觉表面。使用 tool_choice 强制或禁止工具而不是希望,拒绝调用你从未定义的工具,并保持跟踪,因为这是一个带着训练轮的智能体,失败模式只会从这里变大。