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

Agents(智能体) ​

docs.scrimba.com

**智能体(agent)**是指模型在一个循环中运行,不断挑选下一个要调用的工具,直到任务完成。本章接下来要讲的,就是这到底意味着什么、它在哪些地方能真正发挥价值,又在哪些地方会悄悄让你付出代价。

智能体循环 ​

在工具调用中,模型调用一个函数,你把结果发回去,就完事了。但有些任务需要好几个步骤,而且你事先并不知道具体要走哪些步骤。"找到最便宜的机票并预订"可能意味着要先搜索、再比较、然后查日历、最后预订,而这个先后顺序得由模型在执行过程中自己摸索出来。

所以,不再是处理单次工具调用,而是把整个过程放进一个循环里。这个不断重复的过程就是**智能体循环(agent loop)**——同一轮流程反复执行,直到任务完成:

  1. 把对话内容和可用的工具列表发给模型。
  2. 模型要么调用一个工具,要么给出最终答案。
  3. 如果它调用了工具,就执行工具、把结果加入对话,然后回到第 1 步。
  4. 如果它给出了最终答案,就停止。

每一轮,模型都会看到到目前为止发生的一切,包括之前工具调用的结果,然后据此决定下一步该做什么。这就是与工具调用的全部区别所在:你要不断循环下去,直到模型自己认为任务已经完成。

真正的变化在于由谁来掌舵。在此之前的每一章里,都是你的代码在决定控制流:你决定什么时候调用模型、什么时候运行工具、接下来做什么。而智能体把这个决定权交给了模型。这个循环不断地问"接下来该做什么?",而模型每次给出的预测就是答案。你用可控性换来了灵活性,而这笔交易正是它既能带来更大能力、也会带来更大风险的根源。

从底层来看,其实没有发生什么新鲜事。本质上还是同一种预测机制,只是上下文在不断增长——工具调用那个来回往复的过程,反复上演罢了。模型并不会先制定一个计划再照着执行。每一轮它只是根据当下能看到的信息,预测出那个"最佳的下一步",然后循环再次运行。

当这一切顺利运转时,看起来就像是在朝着一个目标进行推理。而当它不顺利时,智能体就会在原地打转,或者一路走进死胡同,因为整个系统里没有任何一部分在真正掌管全局计划。

Juno智能体循环 智能体就是把工具调用的循环反复执行:把对话和工具发过去,模型调用工具或给出最终答案,你执行工具并把结果反馈回去,如此往复直到它给出答案。每一轮模型都能看到之前所有的结果,并据此挑选下一步。相比普通的工具调用,唯一新增的想法就是"循环",而不是只处理一次调用。

在工具调用中,你处理一次调用并返回结果。而智能体就是把这个来回过程包进一个循环里,一直运行到模型自己判断任务完成为止。这个循环就是**智能体循环(agent loop)**。这里的关键转变是:不再把步骤顺序硬编码进代码,而是让模型每一轮自己选择下一步。

这个循环分四步:

  1. 把对话内容加上工具列表发给模型。
  2. 模型返回工具调用,或者返回最终的文字答案。
  3. 如果它要求调用工具,就执行这些工具,把每个结果都附加到对话里,然后循环。
  4. 如果它给出了答案,就停止并返回。

让这一切运转起来的机制是:每一轮模型都能看到完整且不断增长的对话——你最初的请求、它发起的每一次工具调用、以及每次返回的结果。这些累积下来的上下文,就是智能体唯一的"记忆"。每次预测都是基于到目前为止的整段历史,这也正是它能够串联起依赖前面结果的多个步骤的原因。

有个细节值得早点提出来:第 2 步返回的可能是一次性的多个工具调用,而不是只有一个。当这些调用彼此不相互依赖时,比如查询三个城市的天气,模型可以在一轮里把三个都请求出来,你就一起执行它们。而当某次调用需要用到前一次调用的结果时,模型就必须先等到那个结果出来才能发起下一次请求,于是这些调用就分散在了不同的轮次里。这个循环对两种情况都能处理:不管这一轮返回了什么调用,都执行它们,把所有结果都喂回去,再让模型继续预测。

在这整个过程里,没有任何全局计划存在。模型只是根据当下能看到的信息去预测最合适的下一步,而不是依据一张覆盖整个任务的地图。这就是智能体和你之前写过的那些固定流水线之间的区别所在,也正是为什么同一个循环,既能串联起层层推进的研究步骤,也可能陷入原地打转的原因。

Juno智能体循环 智能体就是把工具调用的来回过程放进一个循环里:把对话加工具发过去,模型调用工具或给出答案,你执行工具并把结果反馈回去,重复直到它给出答案。不断增长的对话就是智能体唯一的记忆,所以每一步都建立在此前所有结果的基础上。一轮里可能同时返回若干个互不依赖的工具调用,也可能因为彼此依赖而分散在不同轮次逐个返回。全局计划无处存在:每一步都只是对下一步的一次全新猜测。

智能体就是把工具调用那个来回过程放进一个循环里,并把方向盘交给模型——也就是**智能体循环(agent loop)**:模型每一轮挑选下一个工具,你执行它、把结果反馈回去,如此往复,直到它给出最终答案而不再发起调用为止。这个循环本身只有四步,而且这四步并不是重点所在。真正值得关注的是,这种架构能带给你什么,以及它给不了你什么。

循环过程:

  1. 把当前的完整对话加上工具的模式定义发出去。
  2. 模型返回工具调用,或者返回最终答案。
  3. 执行任何调用,把结果附加进去,循环。
  4. 一旦拿到最终答案,就返回。

单轮可能返回多个工具调用。当模型判断这些调用彼此独立时(比如三次互不相关的查询),它会并行地发起这些调用;而当某个调用需要先用到另一个调用的输出时,它就会跨轮次串行地发起。你无法从外部强行控制这一点:工作是否能并行化,是模型基于它推断出的任务性质做出的判断,所以你以为可以并行的任务,返回时可能是串行且更慢的;而你以为该串行的任务,返回时可能是一整批,需要你准备好一起执行。

这里有一个决定性的事实,几乎所有故障模式最终都能追溯到它。没有全局计划。模型并没有拿着一张任务地图照章执行。每一轮,它都只是根据当前的对话预测下一步,而这段对话就是整个系统的全部状态——你的提示词,加上到目前为止的每一次调用和结果,全部累积在一起。

每一步都信任前一步,却没有任何机制去检查整体是否正确。这就是为什么一个看起来像是在朝目标推理的智能体,实际上只是在做一系列局部看似合理的猜测——当它运气好时,这些猜测碰巧串成了一条正确的路径。

现在需要牢记一个后果,因为后面讲护栏的部分会用到它:因为状态只是一份不断增长的记录,而每一步都基于这份记录做判断,所以早期出现的一个错误工具结果不会被纠正,反而会被后续步骤当作基础继续叠加。错误会一路向前传播、不断累积,而循环中的任何一个部分都没有机会察觉到这一点。做智能体的可靠性工作,基本上就是在跟这一个特性作斗争。

Juno智能体循环 智能体就是把工具调用的来回过程放进一个循环里,由模型掌舵:它挑选下一个工具,你执行它、把结果反馈回去,重复直到它给出答案。整个状态就是那份不断增长的记录,其中没有任何全局计划,只有一连串基于上一步做出的下一步猜测。当模型判断多个调用互不依赖时,一轮可能返回并行调用;当它们相互依赖时,则会返回串行调用,而这一点你无法控制。记住一件事:每一步都信任前一步,所以早期出现的错误结果会一路向前累积,而没有任何机制去检查整体。

一个最小可用的智能体 ​

下面是用代码写出来的这个循环。它复用了工具调用一章里的工具定义和实现,并把它们包进一个循环里,这个循环会一直运行,直到模型不再请求调用工具为止。

python
import json

def run_agent(user_text, tools, tool_impls, max_steps=6):
    messages = [{"role": "user", "content": user_text}]
    step = 0

    while step < max_steps:
        response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
        message = response.choices[0].message
        messages.append(message)

        calls = message.tool_calls
        if not calls:
            return message.content  # 没有要调用的工具:这就是最终答案

        # 执行所有请求的工具,并把结果反馈回去
        for call in 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)})

        step += 1

    return "已停止:智能体达到了步数上限。"  # 安全网

从头到尾读一下:调用模型,把它返回的消息推入对话,如果它请求了工具,就把所有工具都执行一遍并把结果附加进去,然后再循环一次。当模型返回的是文本而不是工具调用时,那就是最终答案,直接返回即可。

max_steps 这个值设定了一个**步数上限**,也就是防止循环失控的安全网,下一节要讲的正是这个话题。这里的写法对应的是某一家服务商的 SDK;不同服务商的字段名会有所不同,但循环本身在哪里都是一样的。

Juno一个最小可用的智能体 一个最小可用的智能体,就是围绕模型调用搭建的一个循环:运行模型,执行它请求的任何工具,把结果反馈回去,重复。当模型返回文本而不是工具调用时,这段文本就是最终答案。步数上限是必须有的安全网,而不是事后再加的附加项。

下面是用代码写出来的这个循环,复用了工具调用一章里的工具定义和实现。它会一直运行,直到模型不再请求工具,或者达到步数上限为止。

python
import json

def run_agent(user_text, tools, tool_impls, max_steps=6):
    messages = [{"role": "user", "content": user_text}]
    step = 0

    while step < max_steps:
        response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
        message = response.choices[0].message
        messages.append(message)

        calls = message.tool_calls
        if not calls:
            return message.content  # 最终答案

        for call in calls:
            args = json.loads(call.function.arguments)
            try:
                result = tool_impls[call.function.name](args)
            except Exception as err:
                result = {"error": str(err)}  # 暴露失败信息,而不是让程序崩溃
            messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

        step += 1

    return "已停止:智能体达到了步数上限。"

这个写法对应的是某一家服务商的 SDK,字段名会有所不同,但每个循环都包含这几个部分:把模型返回的消息附加进去,判断它是否请求了工具,执行被请求的工具,把每个结果作为一条 tool 消息、按调用 id 对应地反馈回去。

比起最原始的版本,值得一做的一处改动是在工具调用外面加上 try/except。当一个工具执行失败时,不要让这个异常把整个循环搞崩。捕获这个异常,把错误信息作为工具结果反馈给模型。这样模型在下一轮就能读到"这次查询失败了",并据此做出调整:换一组参数重试、尝试另一个工具,或者告诉用户它无法完成这个任务。程序崩溃不会给模型任何恢复的机会;而这种**把错误当结果处理**(error-as-result)的模式,能让它继续留在循环里,并拿到可以据以行动的信息。

运行这段代码时,有两件事值得留意。第一,对话内容只会不断增长。每一轮都会附加模型的消息和每一次工具的结果,所以到第五步时,模型要重新读一遍第一到第四步的全部内容。这既是智能体的"记忆",也是它的"账单":每一步都要重新发送一遍累积起来的完整上下文,所以每一步的 token 成本会随着任务推进而不断攀升。一个十步的智能体,绝不等于十次便宜的调用,而是十次调用叠加在一份越来越长的对话记录上。

第二,正因为这种增长,max_steps 的意义不只是防止无限循环。要根据任务本身来设定这个值。一个天气问题只需要两三步;而一个研究型任务可能需要十步。把上限设得比一次健康运行所需的步数稍高一点,这样合理的长任务能够顺利完成,而卡住的任务在耗尽你的预算之前,还是会被及时截断。

Juno一个最小可用的智能体 这个循环是:把模型的消息附加进去,判断它是否请求了工具,执行工具,把每个结果作为一条 tool 消息反馈回去。给工具调用包上 try/except,把错误反馈回去而不是让程序崩溃,这样模型才能重试或恢复。要留意对话只会不断增长:每一步都要重新发送整段记录,所以每步的 token 成本会随任务推进而攀升。把 max_steps 根据任务设定,比一次健康运行所需的步数稍高一些。

这个循环,复用了工具调用一章里的工具层。写法对应某一家服务商的 SDK,字段名会不同,但结构不会变。

python
import json

def run_agent(user_text, tools, tool_impls, max_steps=6):
    messages = [{"role": "user", "content": user_text}]
    step = 0

    while step < max_steps:
        response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
        message = response.choices[0].message
        messages.append(message)

        calls = message.tool_calls
        if not calls:
            return message.content

        for call in calls:
            args = json.loads(call.function.arguments)
            try:
                result = tool_impls[call.function.name](args)
            except Exception as err:
                result = {"error": str(err)}  # 模型会读到这个信息,并可能在下一轮据此调整
            messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

        step += 1

    return "已停止:智能体达到了步数上限。"

这个最原始的循环本身没有问题,但没法直接拿去上线,而这两者之间的差距,正是本节要讲的内容。这段代码在生产环境里会暴露出三个特性。

暴露错误,绝不崩溃。这个 try/except 把失败的工具结果作为模型可以读取的信息反馈回去。这样做是对的,但要清楚自己在做什么:你是在把一段错误字符串交给模型,并信任它能理智地做出恢复。它可能会重试同一个错误的调用,也可能想出一个不同的办法,或者干脆道歉然后停下来。

把错误反馈回去能让智能体继续运行,但并不能让恢复变得可靠。对于任何关键业务,都要对每个工具的重试次数设上限,把反复失败当作一种需要你专门处理的终止状态,而不是指望模型凭感觉去解决的循环。

对话记录只会不断增长,而这带来了两种叠加的成本。一是每步的 token 成本会攀升,因为每次调用都要重新发送整段累积的历史,所以一次长时间运行,每一轮付出的账单都在上涨,而不是持平的。二是对话越长,模型在决定下一步时要读的旧工具输出就越多,这正是上下文窗口一章提到的**"中间遗失"(lost-in-the-middle)**效应开始发挥作用的地方——也就是模型对埋在长上下文中间的信息关注度会降低:那些本应约束智能体行为的早期指令,会被不断累积的结果淹没,得到的关注也越来越少。运行时间长的智能体不会大张旗鼓地失败,它们只会悄悄地跑偏。

max_steps 是一个预算,而不是一道防止程序出错的关卡。它存在的原因是:模型自己决定何时停止,而它也可能决定错了——在某一步上打转、在两个工具之间来回切换、或者一路走进死胡同。

这个数字代表的是一个实实在在的成本上限。最坏情况下的花费大致等于 max_steps 乘以运行接近结束时那种全量上下文调用的成本,所以对一个上下文很重的任务把上限设得宽松一些,账单也会同样宽松。要根据合理运行所需的最长步数,再加上一些余量来设定这个值;当运行经常触及这个上限时,应该发出警报(这说明智能体卡住了,而不是说明上限设得太低),不要反射性地把这个数字调高来掩盖一个跑不完任务的智能体。

Juno一个最小可用的智能体 这个最原始的循环本身没有问题,但没法直接拿去上线。把工具错误反馈回去,让模型能够据此调整,但要对重试次数设上限,把反复失败当作终止状态来处理,因为暴露错误并不能让恢复变得可靠。对话记录只会不断增长,所以每步的 token 成本会攀升,你早期设下的指令也会被旧结果不断淹没,直到智能体逐渐跑偏。max_steps 是一个成本上限,而不是防止出错的关卡:把它设为最长的合理运行步数加上余量,如果运行经常触及这个上限,说明智能体卡住了,不要反射性地把这个数字调高。

停止条件与护栏 ​

一个由模型自己决定何时停止的循环,也可能会停不下来。它可能会不停地调用工具、反复重复自己,或是一路走进死胡同。因为每一步都是一次按 token 计费的模型调用,一个失控的智能体会很快烧掉大量的金钱和时间。

所以,护栏(guardrails)——也就是你给这个循环设下的各种限制——是设计中本该包含的一部分,不是最后再补上去的润色步骤:

  • 设置步数上限。 就像 max_steps 那样,限制循环次数。一旦达到上限,就停止并返回。
  • 验证工具的输入参数。 参数是模型自己选出来的,所以在执行任何有实际后果的操作之前,一定要先检查这些参数。
  • 限制工具的能力范围。 只给智能体它真正需要的最小权限。一个只能"读"的工具,比一个能"删"的工具更安全。
  • 留意成本。 每个任务往往要经过多次模型调用,成本会很快叠加起来,所以要跟踪 token 用量并设定预算。

把这些串联起来的一条线索是:模型是在临场随机应变地走出这条路,所以你要提前设好它自己不会设的那些限制。假定智能体至少会犯一次错,然后让这次错误变得可以承受。

Juno停止条件与护栏 智能体自己决定什么时候停下来,所以它可能永远停不下来,而每一次循环都要花掉 token。一定要设置步数上限,验证模型选出的参数,并且只给工具它真正需要的最小权限。要跟踪成本,因为每个任务往往要经过多次模型调用,很快就会累积起来。

一个由模型自己决定何时停止的循环,可能会停不下来,而因为每一步都是一次按 token 计费的模型调用,一个卡住的智能体会很快烧光预算。护栏是设计中本该包含的一部分。需要接入的四项护栏是:

  • 设置步数上限。 用 max_steps 限制循环次数,一旦达到就干净地返回。这是你抵御失控循环的硬性止损线。
  • 验证工具的输入参数。 参数是模型自己选出来的,它可能选错,也可能被诱导选错。在任何有实际后果的工具运行之前都要检查:一个没带 id 的删除操作、一个金额为负数的转账、一个超出允许目录范围的路径。
  • 最小权限原则。 给每个工具设定任务所需的最窄能力范围。只读胜过读写,一个范围受限的查询胜过原始数据库访问权限,一份固定的收件人名单胜过任意发送权限。
  • 成本上限。 跟踪整次运行的总 token 用量,而不是只看单次调用,一旦某次运行超出预算就停止。

把这些串联起来的关键点在于:你的工具收到的参数来自模型,也就是说,间接地来自模型读到过的任何文本。如果某个工具抓取了一个网页,而这个网页里包含了某些指令,那这些指令现在就进入了模型的上下文,并可能左右下一次工具调用。这就是**提示注入(prompt injection)**:上下文中不可信的文本影响了智能体的行为。防御手段不在提示词里,而在于验证每一个工具的输入参数,并限定每个工具的权限范围,这样即使某个参数完全被劫持,也无法造成真正的破坏。

Juno停止条件与护栏 四项护栏,是设计的一部分,不是事后的润色:max_steps 硬性止损、对每个有实际后果的工具做输入验证、最小权限的工具范围设定,以及整次运行的 token 预算上限。输入验证之所以重要的原因是:工具的参数来自模型,也就是来自模型读到过的任何文本,所以一个抓取回来的、带着指令的网页就是提示注入,可能左右下一次调用。验证输入、限定权限,这样即使参数被劫持,也造不成真正的破坏。

一个由模型自己决定何时停止的循环,可能会停不下来,而每一步都是一次按 token 计费的调用,所以这种失败模式天生就很昂贵。四项护栏(步数上限、输入验证、最小权限、成本上限)都是最基本的门槛。真正值得你花心思的地方,是为什么输入验证是一项安全控制手段,而不只是一个理智检查。

工具的参数是模型的输出,而模型的输出取决于上下文里的一切内容,包括智能体从外部抓取来的文本。一旦智能体读取了一个网页、一份文档、一封邮件,或者一个你无法掌控的工具结果,不可信的文本就进入了它的上下文,并可能携带指令。这就是**提示注入(prompt injection)**:输入中带有敌意的内容,把模型引向了你并不打算发生的动作。在单次的提示词场景里,这只是个小麻烦。而在智能体里,这是一个活生生的可利用漏洞,因为模型的下一步动作是一次会产生真实世界影响的工具调用,而被注入的文本恰恰能左右这次调用。

要把它当成真正带有敌意的东西来设计,因为它本来就是。有三条原则站得住脚:

  • 用允许清单(allowlist)而不是拒绝清单(blocklist)来验证每一个工具参数。 定义清楚一个合法参数应该是什么样子(这种 id 格式、这批收件人、这个目录),然后拒绝一切其他情况。你不可能列举出攻击者能编造出的所有恶意输入;但你能列举出所有合法的输入。
  • 限定工具权限,让一次完全被劫持的调用也是可以承受的。 假设这个参数已经被攻击者控制,然后问自己:这个工具在这种情况下能造成什么后果。如果真实答案是"能发邮件给任何人"或者"能删除任何一行数据",那这个工具的权限范围就太宽了,无论提示词写得多小心都没用。最小权限是即便模型被攻破也依然站得住的控制手段,而提示词根本算不上一种控制手段。
  • 让不可逆的操作必须经过人工确认。 转账、删除数据、给客户发邮件:这些操作都要设上确认这道关卡。智能体只负责提议,由人或者更严格的检查来做最终决定。

之所以这一切都不能靠提示词来解决,更深层的原因是:系统提示词里的指令,和从抓取内容里注入进来的指令,对模型来说是同一类东西——都是上下文里的文本,而模型没有可靠的办法判断哪个应该优先。所以你没法靠"写好提示词"来换取安全。真正站得住的边界在模型之外:经过验证的输入,和权限受限的工具。这一点在安全与局限一章有更全面的展开。

Juno停止条件与护栏 四项护栏都是最基本的门槛;真正值得深思的是输入验证,因为它是一项安全控制手段。工具的参数是模型的输出,取决于它读到的一切内容,所以任何抓取到的网页或邮件都可能携带注入指令,左右下一次调用,而在智能体里这次调用是有真实世界后果的。用允许清单验证输入,限定每个工具的权限,让被劫持的参数也不至于造成破坏,并让不可逆的操作必须经过人工确认。你没法靠提示词换来安全:注入的指令和系统指令,对模型来说是同一类文本,所以边界必须建在模型之外。

什么时候工作流比智能体更好 ​

智能体很让人兴奋,这也正是为什么会有人在根本不需要的时候也想用它。这里有一条简单的原则:如果你已经知道具体步骤,就不要用智能体。把这些步骤直接写成普通代码,也就是一个**工作流(workflow)**,每一步都按固定顺序执行。

工作流更可靠、更便宜,而且调试起来也容易得多,因为路径是你自己掌控的,而不是指望模型能找到它。把智能体留给那些开放式的任务,也就是具体步骤要取决于过程中发现了什么的情况。

很多看起来"需要智能体"的功能,其实更适合用一次结构化输出调用,或者一次手动接好的RAG查询来实现。只有在你无法提前写出具体步骤的时候,才该考虑用智能体。

Juno什么时候工作流比智能体更好 如果你已经知道具体步骤,就把它们直接硬编码成一个工作流,而不是用智能体:这样更便宜、更可靠,也更容易调试。把智能体留给那些开放式的任务,也就是具体步骤要取决于过程中发现了什么。很多所谓的"智能体"功能,其实就是一次手动接好的结构化输出调用或 RAG 查询。

智能体是那个让人兴奋的工具,而这恰恰是它经常被用错地方的原因。判断该用哪一种,只需要问一个问题:你能不能提前把具体步骤写出来?如果能,你需要的是一个**工作流(workflow)**——把这些步骤写成按固定顺序执行的普通代码,而不是智能体。

在智能体吃力的每一个方面,工作流都占优势。它更便宜,因为你只需要一到两次模型调用,而不是一整个循环。它更可靠,因为路径是固定的,而不是每一轮都要重新决定。它也容易调试得多,因为失败会发生在一个你能检查到的确定步骤上,而不是散落在一串由模型自己选择的动作链里的某个地方。

来看一个实际的判断标准。"总结这个工单、给它的紧急程度分类、然后路由到对应队列"这个任务,步骤和顺序都是已知的,所以它是一个工作流:用一次结构化输出调用拿到摘要、分类和紧急程度,然后用普通代码根据结果做路由。而"调查为什么这个客户最近三次订单都失败了"这个任务,具体步骤是未知的,路径要取决于每一次查询发现了什么,所以它是一个智能体。

陷阱在于,很多功能看起来是开放式的,实际上并不是。从文档中提取字段、根据你的文档内容回答问题、给收到的邮件分类:这些听起来像是智能体的活儿,但归根结底其实是一次结构化输出调用,或者一次外面包了一层代码的RAG查询。默认先用工作流,只有当一个任务的不可预测性真的没法用脚本写出来时,才让它去争取用上智能体。

Juno什么时候工作流比智能体更好 判断的方法只有一个问题:你能不能提前把具体步骤写出来?如果能,就搭建一个工作流,把步骤写成普通代码,因为它更便宜、更可靠,而且能在确定的位置调试。"总结、分类、路由一个工单"是一个工作流:一次结构化输出调用加上路由代码。"调查为什么这些订单失败了"是一个智能体,因为路径要取决于每一步发现了什么。默认用工作流,让任务自己去证明它值得用上智能体。

智能体是大家都想用的那个工具,这也正是它经常被用在不该用的地方的原因。判断的原则很简单:如果你能提前把具体步骤写出来,就把它们写出来。这就是一个**工作流(workflow)**——把步骤写成按固定顺序执行的代码——对于任何你了解其形态的任务,它在成本、可靠性和可调试性上都胜过智能体。

原因可以直接追溯到架构本身。智能体没有全局计划,而且会把错误一路向前累积,所以它的可靠性等于每一步都走对的概率相乘,而且没有任何机制去检查整体。而你自己写的工作流,按定义就带着一份计划,你可以对每一步单独检查、测试、验证。工作流出问题时,会出在某一行代码上。智能体出问题时,会出在一整段由模型自己选择的动作记录里的某个地方,你得从这段记录里反推它当时到底在想什么。

现在来看更棘手的情况,因为"我知道不知道具体步骤"只是问题的简单那一半。即便任务是开放式的,智能体也可能是错误的选择——当一步走错的代价太高,高到不能让一个只会猜下一步的东西来做决定时,就是这种情况。一个负责预订旅行、转移资金,或者修改生产环境数据的智能体,任务本身是开放式的,但这恰恰是你不希望让一个无计划、易被注入、错误会不断累积的循环来掌控全局的地方。这种情况下更好的设计往往是一种混合方案:由模型来做规划或提议,但由固定的工作流和人工关卡来执行,这样那个不可预测的部分永远不会直接碰到那个不可逆的部分。

所以真正要问的是两个问题,不是一个。你知不知道具体步骤,以及一步走错的代价是什么?

已知步骤交给工作流。未知步骤但操作成本低、可以撤回的场景,正是智能体能发挥价值的地方。未知步骤而且操作成本高、不可逆的场景,需要的是一种受约束的设计,而不是一个自由运行的循环。

智能体适合那种开放式、且步骤成本低、可撤回的工作,除此之外几乎没有其他适用场景。相当多所谓的"智能体"功能,其实就是一次结构化输出调用或者一次外面包了代码的RAG查询,它们最好继续保持这个样子。

Juno什么时候工作流比智能体更好 是两个问题,不是一个:你知不知道具体步骤,以及一步走错的代价是什么?已知步骤交给工作流,它在成本、可靠性和调试性上都胜过智能体,因为它有一份计划,而智能体永远没有。未知步骤但成本低、可撤回的场景,是智能体能站稳脚跟的地方。未知步骤而且成本高、不可逆的场景需要一种混合方案:模型负责提议,固定的工作流和人工关卡负责执行,这样那个会不断累积错误的循环永远碰不到那个无法挽回的部分。

实际应用 ​

上面那个 run_agent 函数就是全部内容。给它一个问题、一组工具,以及这些工具的实现,它就会一直循环,直到任务完成,或者达到步数上限。一个小任务就能展示出这个循环是怎么发挥作用的:

python
answer = run_agent(
    "北京的天气怎么样?我应该带伞吗?",
    tools,        # 包含来自工具调用一章的 get_weather
    tool_impls,   # {"get_weather": ...}
)
# 智能体调用 get_weather,读到"多云",然后在最后一轮直接给出答案:
# "北京现在多云,气温 18 度,带伞是个不错的主意。"

一个工具,一个循环,一个答案。同样的结构可以扩展到多个工具、多个步骤,而形态不会改变。一旦你能搭建出这种能力的东西,接下来的问题就是:你怎么知道它真的管用?这正是评估这一章要讲的内容。

Juno实际应用run_agent 这个循环就是全部套路:给它一个问题、工具、以及这些工具的实现,它就会一直循环,直到模型不再请求工具,或者达到步数上限。一个工具一个循环,可以扩展到很多个而形态不变。接下来的问题是:你怎么知道它真的管用?这正是评估要讲的内容。

run_agent 函数就是全部套路。给它一个问题、工具,以及这些工具的实现,它就会一直循环,直到模型不再请求,或者达到上限。

python
answer = run_agent(
    "北京的天气怎么样?我应该带伞吗?",
    tools,        # 包含来自工具调用一章的 get_weather
    tool_impls,   # {"get_weather": ...}
)
# 智能体调用 get_weather,读到"多云",然后在最后一轮直接给出答案。

一个工具,一个循环,一个答案,同样的形态可以扩展到多个工具、多个步骤。而不能免费得到的,是你对它管用与否的信心。工作流你可以一步一步测试;智能体对不同输入会走不同的路径,所以昨天跑通了的一次运行,几乎说明不了今天失败的这次运行会是什么情况。

这正是为什么在搭建智能体的同时,还要建一套衡量它的方法:在一组具有代表性的任务上运行它,检查结果,而不是靠一次成功的演示就下定论。把"我试的时候它管用"变成一个可以追踪的数字,正是评估这一章要讲的内容。

Juno实际应用run_agent 这个循环就是全部套路,一个工具一个循环,可以扩展到很多个而形态不变。问题在于信心:智能体对不同输入会走不同的路径,所以一次好的演示几乎说明不了下一次运行会怎样。要建一套在具有代表性的任务上衡量它的方法,而不是靠一次成功就下结论,这正是评估存在的意义。

run_agent 函数就是全部套路,下面这个例子是最基本的形态:一个工具,一个循环,一个答案。

python
answer = run_agent(
    "北京的天气怎么样?我应该带伞吗?",
    tools,        # 包含来自工具调用一章的 get_weather
    tool_impls,   # {"get_weather": ...}
)
# get_weather 返回"多云",模型在最后一轮直接给出答案。

这种形态可以扩展到很多工具、很多步骤而不改变自身,而这正是陷阱所在,因为失败模式也会随之扩展,代码本身却看起来毫无变化。智能体是路径依赖的:不同的输入会驱动出不同的、由模型选择的步骤序列,所以针对某一个输入做的测试,几乎说明不了你还没见过的那个输入会怎样。而采样带来的非确定性,意味着即便同一个输入,两次运行也可能走出不同的路径。

所以,你在搭建智能体的同时该建的,是一种度量手段,而且这种度量和工作流所需要的完全不同。你不是在断言"应该有一个确定的输出";你是在一组具有代表性的任务上做采样,然后观察结果的分布:它有多大比例能在预算内完成、有多大比例能得出正确答案、在哪里会打转、在最极端的情况下要花多少钱。这就是智能体版本的评估,也是唯一能把上面几节提到的可靠性、成本、安全性这些顾虑,变成你能拿出来站得住脚的数字,而不是一次靠运气跑通的演示的办法。

Juno实际应用run_agent 就是全部套路,它的形态可以扩展,而失败模式也随之扩展。智能体是路径依赖的,采样又带来了非确定性,所以针对单个输入的测试说明不了太多,一次好的演示能证明的更少。要用智能体的方式来衡量它:在一组具有代表性的任务上采样,观察结果的分布——有多大比例能在预算内完成、在哪里会打转、最极端情况下要花多少钱。这正是评估存在的意义,也是让上面提到的可靠性和成本顾虑,变成你能拿出来站得住脚的数字的办法。