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

工具使用

docs.scrimba.com

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

模型本身被封闭在一个盒子里。它无法检查今天的天气、在你的数据库中查询用户,或发送电子邮件。它只能从学到的内容预测文本,这些内容在训练时就已冻结,就像 LLM 如何工作 中的那张静止画面。工具使用是让它突破这个盒子的方式,是 agents 构建的基础。

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

模型只产生文本,所以单独来看它无法获取实时数据、访问你的数据库或触发某个操作。工具使用弥补了这一差距:你描述一组函数,模型发出一个结构化的调用请求,你的代码运行它,然后你把结果反馈给模型。模型编排,你的代码执行。

本章是你将构建的每个 agent 的基础机制。在这里理解循环、工具模式和错误处理路径,agents 就会变成这个模式反复应用。

工具使用是概率文本生成器与你的确定性代码的接缝,大多数生产问题都存在于这条接缝上。模型预测一个结构化的调用,你的代码执行它,结果被反馈为更多上下文。所有让这变得困难的事情——验证不受信任的参数、幂等性、额外的往返、幻觉调用、可观察性——都来自那一个交接点。

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

循环

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

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

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

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

循环有四个节拍:发送消息加上工具列表,模型答复或请求调用,你的代码运行函数,结果返回,模型继续。初学者常常遗漏的是这很少是单轮的。模型可以请求一个工具,读取你的结果,然后基于看到的内容请求另一个工具,等等。

所以真实的形状是一个**多轮循环**:不断调用模型并运行它请求的任何工具,直到得到一个没有工具调用的响应。那个最终的无工具响应才是对用户的答案。

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",而不是正常句子。所以当你的问题让一个工具看起来有用时,最可能的下一个输出是那个结构化的**工具调用消息**,API 作为 tool_calls 呈现给你。

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

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

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

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

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

有用的后果:一个工具调用是**约束生成**,模型产生的输出以匹配你提供的模式。那完全相同的机制就像 structured output,你要求模型以你定义的形状返回 JSON。

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

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

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

Juno实际发生的事情 一个工具调用是相同的下一个令牌预测,针对一个模式而不是散文:API 将那个预测表面为 tool_calls。它是约束生成,与结构化输出的机制相同。区别是意图:结构化输出给你数据并停止,工具使用给你数据这样你可以运行某个东西并做出反应。无论哪种情况模型只提议,你的代码是唯一接触任何真实东西的。

一个工具调用是预测文本,不是执行,保持这个想法直就是保护你的方式。模型发出给定上下文中工具的最高概率延续,API 将它解码为结构化调用,你的代码决定是否尊重它。在机制上它是 structured output 附加了一个副作用。

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

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

所以心理模型是一个请求路由器,不是同事。模型路由到一个函数和提议的参数。你的代码认证、授权、验证,只有然后执行。"只有然后"之后的所有东西是这章赚取其分量的地方。

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

定义工具

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

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

parameters 块是一个**模式**,与 structured output 的想法相同:它定义模型必须产生的参数。当模型决定调用 get_weather 时,它发送回一个匹配这个形状的 city 参数。模糊的描述("获取天气")导致模型在错误的时刻使用工具。清晰的描述("当用户询问天气时使用")很好地引导它。

Juno定义工具 工具定义有一个名字、一个描述和一个用于其参数的 parameters 模式。描述实际上是一个提示:它告诉模型何时使用工具,所以要仔细写。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_call_id 标记的 tool 消息这样模型知道哪个结果回答哪个请求。

值得做对的部分是失败。你的工具会抛出:一个坏参数,一个 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。响应形状(tool_callstool_call_idtool 角色)是 OpenAI 风格并因提供商而异,但模式,解析、运行、返回结果或错误,对所有提供商都持有。

Juno处理调用 回复携带 tool_calls:解析 JSON 参数,运行函数,用匹配的 tool_call_id 追加一个 tool 消息。关键在失败:不要让异常冒泡并结束转,捕获它并作为工具结果交回 {"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}

两个更多生产现实:

  • 可观察性:记录哪个工具运行了,用什么参数(编辑秘密),以及它返回了什么,因为当一个 agent 行为不端时工具调用的追踪是重构它实际做了什么的唯一方式。
  • 破坏工具:对任何不可逆的事情,删除、支付、电子邮件,不要让模型的调用是最终权威。把它关在确认之后,一个干运行报告会发生什么,或一个人工批准步骤,并限制凭证这样爆炸半径被限制即使模型是错的或被劫持。

一个模式有效的调用在你的代码授权它之前仍未经信任。 (字段名和重试信封是 OpenAI 风格;纪律对任何提供商都有效。)

Juno处理调用 解析是无聊的部分。在参数和副作用之间:对真实状态验证不只是模式,因为重试会两次触发使写幂等关闭 tool_call_id,以及以数据形式返回错误模型可以作用于而不泄露你的内部。记录每个调用和其参数这样你可以重构一个 agent 实际做了什么。并为任何破坏的东西关闭确认或限制凭证,因为一个模式有效的退款在你的代码说是之前仍是一个陌生人的请求。

实践中

与可重复使用函数相同的流程。注意唯一站在模型和你系统之间的是你写的并控制的代码:

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 中的每个条目来处理。如果你让模型在一个循环中保持调用工具直到完成,每次决定自己的下一步,你得到一个 agent,这正是下章去的地方。

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

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

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 "抱歉,我不能完成那个。"  # 达到转限

三个决定使这生产就绪而不是演示。循环遍历 tool_calls 全部,不是 [0],或你无声地丢弃模型要求的调用。限制 MAX_TURNS 这样模型不能永远循环。并通过 run_tool 路由失败作为错误消息,这样一个坏调用不会撞毁整个交换。

当你停止硬编码步骤而让模型每转决定自己的下一步时,这完全相同的循环成为一个 agent。区别是自主性,不是架构。

Juno实践中 生产处理器是多轮循环有三样东西接线:循环遍历 tool_calls 中的每个条目不只是第一个,限制转这样它不能永远循环,以及以工具消息形式返回失败这样一个坏调用不会撞毁运行。相同循环,更多自主性,你有一个 agent。架构不改变,模型只得到决定自己下一步的纬度。

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

工具是正确答案当行动确实依赖于模型对自然语言的判断时:用户意思哪个订单,这是否计作退款案例,搜索什么。它是错误答案当路径是**确定性**的,相同输入总是产生相同调用。如果一个请求总是触发相同调用有相同可导出的参数,在代码中接线那个并跳过往返;你移除一个幻觉表面、一个延迟跳、一个令牌成本零损失。交给模型一个工具买灵活性并为非确定性付费,所以只在灵活性是要点时花费。

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(强制、禁止或释放工具使用的参数)在你知道需要一个工具时要求一个或在你知道没有应该触发时禁止它们,而不是让每转听天由命。
  • 保持可观察性追踪,工具、参数、结果、转数,因为一个自主循环只有在你能重放它实际做了什么时才可调试。

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

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