Agent 的最小运行链:三个外部零件

所属:第一部分 · 模型和 Agent 到底是什么(第 1–4 章) 前置:第 1 章(猜下一个词、跨调用状态来自外部、上下文、系统提示) 本章要建立:工具调用请求不等于工具已经执行;多轮连续性依赖外部状态与本轮上下文;循环必须同时有正常出口和强制停止条件 本章新概念:工具、参数、工具调用、循环、轮数上限、状态保存、上下文装配


一、上一章留下的问题

上一章拆完之后,里面那台机器是这么一副样子:只靠参数不能保证拿到当前事实、不会自己保存跨调用状态、一次生成结束后不会自己执行动作再继续、可能一本正经地编,而且角色标记也不是安全隔离墙。

可你平时用的 AI 软件明显能连续查资料、读文件,再根据结果接着做。为了看清这个最小运行链路,本章先从模型外面的运行系统里抽出三类职责:声明并执行工具,把多次模型调用接成有出口的循环,以及在每一轮装配有效上下文。本章把它们简称为「三个零件」。

这三个零件一个都不是模型参数本身。有的由你的程序负责,有的可以由模型服务商托管;在实现里,它们也不一定恰好对应三个独立组件。安全、权限、审批、校验和运行记录等生产职责会在后文再加,本章的三件套不是完整架构清单。模型做的仍然是根据输入生成输出;变的是它这次能看到什么、能提出什么动作,以及输出之后谁去接着干。

二、先看一个它做不到的问题

问它:「明天上海下不下雨?」

模型参数里的知识来自训练;一次对话也不会把明天的天气自动写进参数。没有实时数据源时,它的反应通常是两种之一:承认不知道,或者按概率生成一段读着像天气预报的答案。

现在我们要让它答对。接下来这件事的关键在于:我们不会更新模型参数。

三、零件一:工具

工具这个词容易让人误会成「给模型装了一只手」。更准确的说法是:给它一份菜单,让它能点菜。真正动手做菜的是你的程序。

看具体怎么做。你发给模型的那串文字,除了用户的问题,再加一段说明:

你可以使用下面这个动作:   查天气(城市, 日期) —— 返回该城市该日期的天气 需要用它的时候,就单独输出一行,例如:〈调用〉查天气("北京", "今天")

这几行怎么读:有一个动作叫「查天气」,括号里要填它需要的两样东西——城市和日期,这两样东西有个名字,叫参数。最后那一行只是格式示范,填的「北京、今天」跟用户问的事没关系。〈调用〉 是你和模型约好的暗号,你的程序看到这四个字,就知道模型想用工具了。

模型读完这一整串,还是干它那件事:算下一个 token 的概率表,挑一个。因为你刚才那段说明摆在那儿,「输出一行调用」成了概率很高的续写。于是它吐出来:

〈调用〉查天气("上海", "明天")

注意它填进去的是「上海、明天」,不是示范里那行「北京、今天」——城市和日期是它自己从用户那句话里接出来的。

模型没有查天气。它只是生成了一条工具调用请求;请求可以驱动外部程序去查询,但不等于查询已经发生。

接下来是你的程序干活。它先检查请求格式和权限,再去调气象接口(就是提供天气数据的那个 API),拿回「小雨,18–24℃」,然后把这个结果放进下一次调用的有效上下文。

模型这一次读到的是三样东西拼起来的一整串:用户的问题、它自己刚才那行调用、以及「小雨,18–24℃」。它接着猜下一个 token,吐出:

明天上海有小雨,18 到 24 度,出门记得带伞。

(「出门记得带伞」是它自己添上去的,工具返回的数据里只有小雨和温度两项。)

把两种情况摆在一起:

没给工具 给了工具
模型看到的输入 明天上海下不下雨? 明天上海下不下雨?+ 一份可用动作说明
模型生成的第一项输出 一段听着像天气预报的话 一条「我要调用 查天气(上海, 明天)」的请求
谁真的去查了 没有人 你的程序
那个天气数据从哪来 概率表 气象接口
能否核对答案 没有外部数据依据 有了外部数据依据;仍取决于参数、数据源和执行是否可靠,以及模型有没有正确使用结果

这就是工具。 它的定义可以写成一句话:一个事先声明给模型、由模型外部系统负责执行的能力。 自定义工具通常由你的程序执行;搜索、代码执行这类内置工具也可能由服务商执行。无论执行者是谁,模型生成的工具调用只是请求,不等于动作已经完成。

主流模型接口通常会提供专门的位置声明工具,并把「调哪个工具、参数填了什么」作为结构化输出返回;具体字段和托管方式因服务商而异。以 OpenAI 的 function calling 流程为例,模型返回的是一项工具调用请求,应用执行后再把工具输出交回模型。你不必像上面的教学例子那样,真的靠 〈调用〉 四个字从普通正文里识别请求。

对应用来说,结构化工具调用请求和普通回答是两种不同的输出类型;但它们共享一个关键边界:模型负责生成请求,外部运行系统负责校验和执行。 这套机制通常叫工具调用(tool call,也有人叫 function calling)。

有一句话值得单独记住:工具不是模型参数里凭空长出来的能力,是运行系统暴露给它的能力。 一个 Agent 能不能查天气、读文件、发邮件,取决于这次运行给了它什么工具和权限。不同模型、接口和产品可能预置不同工具,也会在「会不会正确使用工具」上表现不同。

顺带说清楚这套机制在哪儿会失灵,免得你以为给了工具就万事大吉:

当运行系统把工具选择留给模型时,「要不要调、调哪个、参数填什么」都由模型生成;运行系统也可以禁止工具、强制调用某个工具,或只开放一小组工具。无论怎样配置,模型生成的调用请求都没有硬正确性保证:菜单扩展了可用动作,却不会自动保证它点对。

四、零件二:循环

把问题换成一个稍微复杂点的:「我周末想去上海,哪天天气好?」

这件事做完至少需要两类动作:取得两天的天气,再比较并回答。为了看清循环,下面故意让模型先查周六、拿到结果后再查周日。真实接口通常也允许它在同一轮同时提出两条互不依赖的查询;并行能少等一轮,但「结果回来后继续判断」仍然需要下一次模型调用。

循环本身简单得让人意外,就两句话。这里先把「一轮」理解成一次模型调用及其输出处理;第 4 章再给出适用于多条并行调用的精确定义。

  1. 把当前这一整串文字发给模型。
  2. 解析模型返回的输出。有工具调用请求,就先校验并执行,把结果接回有效上下文,再回到第 1 步;没有调用请求且已有可交付的回答,就正常结束。

这里为了看清顺序,按「一轮一个调用请求」来画。真实接口可能在一轮返回多个互不依赖的请求,但控制问题不变:运行系统仍要逐项校验、执行、回填,再决定是否继续。

跑一遍:

第几轮 这一轮新接进那串文字的东西 模型吐出来的 你的程序做了什么
1 我周末想去上海,哪天天气好? 〈调用〉查天气("上海","周六") 调气象接口,拿到「小雨」
2 工具结果:小雨 〈调用〉查天气("上海","周日") 调气象接口,拿到「晴,20–26℃」
3 工具结果:晴,20–26℃ 周日更合适,晴天 20 到 26 度…… 没有工具调用请求,检查后把回答交给用户,循环结束

三轮,一件事。(「周六」对应几月几号,要么工具自己能消化这种说法,要么由运行系统换算成日期;除非当前日期已经被放进上下文或另有日期工具,模型没有可依赖的实时日期来源。)

你可能会想:这三步我自己按顺序写死不就行了,要模型在中间干什么?问得好——先记着这个疑问,本章第九节回来拆它。

这里有两件事要说清楚。

第一,模型没有一份跨调用自动保留的「我第 1 轮查过周六」记忆。 它第 3 轮之所以能用到周六下雨,是因为这次有效上下文里仍有前面的调用与结果。你的程序可以提交这些内容,也可以让 API 根据会话状态接续。如果有效上下文里不再有这条结果,模型就不能可靠地使用它。

第二,这个循环必须有办法停下来。 停止的条件通常是三种:

停止条件 什么时候触发 不设会怎样
没有调用请求,且已有可交付的回答 模型认为可以回答了,程序检查通过 ——(这是正常出口)
达到轮数上限 转了 N 圈还没结束 一个绕圈的模型会一直烧钱,直到你发现
程序自己叫停 同一个工具连调五次、工具一直报错 循环在原地打转,账单照涨

「达到轮数上限」这一条,是生产系统通常都会设的一道闸。它不是可有可无的保险:循环越长,调用次数越多,有效上下文通常也越长;即使接口托管了会话状态或命中了提示缓存,停不下来的循环仍会持续消耗时间和费用。

五、零件三:上下文装配

上一章给过「上下文」这个词:这一次调用时模型能看到的全部内容。这一章要补上的是——这些内容先由谁保存,本轮又由谁从中挑选。

答案是模型外面的运行系统。状态保存负责让历史和工具结果在调用之间继续存在;上下文装配负责从已保存状态和新输入中,选出这一轮要让模型看到的内容。没有被保存的信息,下一轮就装不回来。你的程序可以自己负责,也可以把一部分状态管理交给 API。无论实现放在哪儿,每轮都要确定这一轮的有效上下文,至少涉及四类内容:

装什么 举个例子 不装会怎样
系统提示 「你是一个帮人查天气、安排行程的助手,回答简短」 缺少这个应用的目标与约束,只能依赖产品默认行为和其余上下文
可用工具的说明 查天气(城市, 日期) 干什么用、参数怎么填 它不知道这次运行提供了这个工具,只能根据其余上下文回答或承认不知道
到目前为止的相关往来 用户的问题、调用请求、工具结果 缺掉下一步依赖的信息,它就无法可靠接着做
这一轮的新输入 用户刚说的话 它看不到用户这一轮说了什么

系统提示、工具合同和当前步骤所需的状态,要以接口支持的形式继续可用;这不等于客户端必须逐字重传,也不等于所有内容都要永久原样保留。随着循环转下去,「到目前为止的相关往来」很容易越积越多——不光是对话,工具返回的内容也会往里堆。查天气返回一行字还好,抓一个网页正文回来就是几千 token,查一次数据库可能上万。

所以「装配」这个动作不是「把手上有的全倒进去」。它是一个选择,而且每一轮都要重新选一次。判断标准是这一句:

这一轮,为了让它做对下一步,最少需要让它看到哪些东西?

查天气那个例子看不出这个选择——工具只返回一行字,全装进去也不占地方。把任务换大一点:让它把某个题目的公开报道读一遍,挑出讲同一件事的段落。两篇报道都抓回来之后,那串文字里攒着三样东西:

这一轮要让它做的事是:把两篇里讲同一件事的段落挑出来。 于是至少有三个决定要当场做:

手上这一样 装还是不装 为什么
第一篇正文(三千字) 保留待比较的原文;还不知道相关段落时可暂留全文 只留摘要可能把这一轮要比较的细节磨掉
第二篇正文(两千字) 同上 同一个理由:这一轮要用到原文细节
「接口超时」那一行 下一步要处理这次失败时装;否则只留在外部状态或日志 让后续步骤知道刚才失败过,再按重试策略决定是否重试

再看下一轮。下一轮要做的事换成「照着挑出来的结论写一段话」,两篇全文通常不必继续全量保留,但若成稿要附出处,仍应留下结论、支持它的原文片段和来源;超时记录若已不再影响下一步,再删掉。同样一堆材料,这一轮该装的和下一轮该装的不是同一份。

上下文装配最依赖当前任务和当前轮次,因而没有一份固定模板。每往里多塞一样东西,通常都会增加处理成本,也可能让真正需要的信息更难被稳定利用;但删的时候也不能整段砍掉——被砍掉的那一段里,可能正好有下一步需要的一句话。

六、三个零件拼起来长什么样

Agent 的三个零件都在模型参数之外:运行系统装配有效上下文并调用模型;模型返回工具调用请求时,外部执行器执行工具、把工具结果放回上下文,再转下一圈;模型返回可交付答案时交给用户,触发硬上限时则强制停止,任务可能尚未完成。图用「你的程序」表示最容易看懂的自建版本,实际产品也可以把状态管理或内置工具交给服务商

看这张图的时候只需要盯住一件事:中间那个方块(模型)根据本轮有效上下文生成回答或工具调用请求;请求校验、工具执行、结果保存、下一轮装配和强制停止都发生在外部运行系统里。

七、这三个零件补上了上一章的哪些毛病

逐条对一下账。

上一章那台机器的毛病 哪个零件在补 补到什么程度
不能只靠参数保证当前事实 工具 提供外部数据通道——还要满足调用正确、数据源可靠且结果被正确使用
不会自己保留跨调用状态 状态保存 + 上下文装配 让相关状态在下一轮重新可用——不是把状态写进模型参数
一次生成后不会自己执行动作再继续 循环 把多次调用和外部动作接起来——代价是必须管理正常出口与强制停止
可能一本正经地编 工具 + 校验 提供可核对的外部依据——工具结果本身也可能错误,模型也可能不用或误读
角色标记不是安全隔离墙 一个都没补 工具还会引入更多不可信内容

前三个缺口被外部运行系统部分补上,安全缺口却没有随能力一起消失。

上一章说过,角色标记能提示内容来自系统、用户还是工具,却不是安全隔离墙。现在加了工具,工具会把外面世界的内容搬进上下文——网页正文、别人发来的邮件、一份你没读过的文件。模型知道它们来自工具,但仍可能把其中的恶意文字误当成该执行的要求。

工具能接触的外部来源越多,有机会进入上下文的不可信内容通常也越多。 这不是只靠把提示词写得更小心就能解决的问题,而是扩展能力时必须同时管理的风险。后面的安全章节会用权限、隔离和动作确认来限制它可能造成的后果。

八、三条结论

一,本课用模型 + 工具 + 循环 + 上下文装配,描述一个可执行 Agent 的最小运行链。 三类外部职责可以由你的程序或服务商承担,也可以出现在固定工作流里;它们不是完整生产架构清单。

二,工具调用请求不等于工具已经执行。 运行系统决定开放哪些能力与权限,并校验、执行请求;当工具选择留给模型时,模型再决定是否调用、调用哪个并生成参数。工具给了它一份菜单,没有保证它一定点对。

三,循环让多次调用连续起来,但连续性来自状态保存和本轮上下文装配。 运行系统既要回答「这一轮最少需要让它看到什么」,也要设置正常完成、轮数上限和错误叫停;强制停止时,任务可能仍未完成。

九、这一章引出的问题

三个零件搭起来,一个问题会立刻冒出来:这个循环,该让模型自己做多少主?

同样是「查天气 + 订酒店」,你可以把路线定死——先查天气、再按天气挑一天订酒店、最后生成一封确认邮件,每一步做什么都是你写好的,模型只负责填空;你也可以只给目标、工具和硬边界,不预先写死具体动作顺序,让模型根据每轮结果提出下一项动作和收工时点。

这两种做法今天都被叫做 Agent。 但它们花的钱、出错时的排查难度、能不能承诺一个交付时间,差得非常远。下一章把这两种东西分开,并给出一条判断该用哪一种的规则。

十、自检题

  1. 你给模型配了一个「查天气」的工具,它输出了一行 〈调用〉查天气("上海","明天")。这个瞬间,天气查到了吗?
  2. 有人说「这个模型能查数据库,那个模型不能」。这句话哪里说得不准确?
  3. 循环跑到第 5 轮时,你为了省钱,把第 1 轮到第 3 轮的内容从上下文里删掉了。可能出什么问题?
  4. 为什么生产系统通常需要设一个轮数上限?只是为了防止程序卡住吗?
  5. 为什么上下文装配没有一份适用于所有任务和轮次的固定模板?
  6. 加上工具之后,为什么「角色标记不是安全隔离墙」这个问题会更严重,而不是被缓解?

十一、参考答案

先自己答完再往下看。

  1. 没有。 那一行只是模型生成的工具调用请求。接口可以把它封装成结构化对象,但请求仍不是执行结果;真正去调气象接口的是外部运行系统。
  2. 不准确的地方在于没有说明这是产品或运行配置提供的能力。同一个模型,这次运行暴露了数据库工具就能提出查询请求,没暴露就不能;不同产品也可能预置不同工具。还要继续问:谁执行这个工具、模型能访问哪些数据、有没有权限限制。
  3. 它会失去被删掉而又没有被摘要保留的信息。 第 5 轮能利用什么取决于这一轮的有效上下文。删掉前三轮后,它可能重复调用工具,也可能与之前矛盾;但如果程序已经把关键结论压成摘要,删掉原始过程完全可以是正确选择。
  4. 不只是防卡住,也是给时间和费用设硬上限。 每多一轮就至少多一次调用;有效上下文通常还会随结果累积,使后续轮次更贵、更慢。服务端状态和提示缓存可能降低传输、计算或价格,却不会让无限循环变成免费。没有上限时,模型还可能在两个工具之间来回横跳很久。
  5. 因为下一步要完成的职责不同,所需证据也不同。比较原文时要保留细节,写带出处的结论时要保留结论、证据片段和来源;错误记录只有在仍会影响重试或解释时才有用。装配要在每一轮回答「这一轮最少需要看到什么」:多装,通常更贵、更慢,还可能让关键信息被淹没;少装,又可能因为缺一句承重信息而做错下一步。
  6. 因为工具是往上下文里搬外部内容的通道。网页正文、邮件、文件内容虽然带有工具来源标记,却仍要交给同一个模型理解;模型可能误把其中的恶意文字当成要求。能力越强、能读的来源越多,接触不可信内容的机会也越多,所以还要靠权限和动作确认限制后果。

上一章:第 1 章 模型到底是什么:一台根据上文猜下一个词的机器 下一章:第 3 章 「Agent」——智能体是什么