跳到主要内容

托管的那一套 — 助手、线程、运行,和那个会把你卡住的状态

这一章讲三件事: 把上下文交出去之后,你手里还剩什么; 一次调用为什么变成了四个对象;以及漏掉最后一步回话会发生什么。

它在全书链条里的位置: 第 02 至 05 章讲的是「什么都自己管」那一路。 这一章讲的是同一件事的另一路:什么都交给对方管。 两路的机制完全一样,只是谁拿着方向盘不同。第 13 章第 9 节会拿这一章的代码去对照今天。

1. 换一种分工:上下文不归你管了

这一节先说清这一套和前面几章的分界线在哪。

书对这套东西的定位很直白:它是一个语言理解和生成平台, 作者读完官方说明后自己补了一句——你会不会觉得它有点 agent 的意思1

它和前面几章最大的差别只有一条:上下文不归你管了。 书说:基于它一次性调用多个函数的能力,开发者无须管理对话线程和上下文内容, 可以直接将这些工作移交给对方处理2

这条听起来全是好处,但书自己给了代价,而且给得很坦白:

线程对信息量没有限制,你可以往里加任意数量的消息; 对方会用压缩之类的手段确保请求装得下模型一次能读的长度。 这是一件好事,也是一件坏事——虽然不用自己管上下文,降低了复杂性, 但是伴随这种便捷性的是你无法有效控制运行的成本3

请把这段话和第 02 章第 1 节那笔账放在一起读。 那一章说, 一次短对话是 89 进 254 出、合计 343;你每一轮发多少,自己数得清。 而这一套里,发多少由对方决定,你只看到账单。

2. 四个名词:助手、线程、消息、运行

这一节把四个对象一次讲清,后面全章都在用它们。

书给的调用流程只有四步4,每一步造一个对象:

造什么它是什么一句话说清
助手一个配好指令、模型和工具的角色它是「谁来干活」
线程一次对话它是「这一摊事」
消息一条用户的话它是「这一句」
运行让某个助手在某个线程上跑一次它是「开工」

创建助手时能配的东西,书列了五样:名字、指令(告诉它该怎么表现)、 工具、用哪个模型、以及自定义函数5

线程这个名字最容易误解,先纠正一下。 它和程序里那个「多线程」的线程没有关系。 书给的解释很好懂:一个线程就代表和模型的一次对话, 就好比你在网页版聊天里开启一次新的会话——从打开到关掉的这一整段来往6

书还提醒了一个很实际的坑:助手创建完会产生一个编号,后续直接用这个编号调它; 不要重复运行创建代码,否则会生成一堆功能重复、编号不同的助手7。 线程同理8作者自己就踩过,而且在书里承认了。

3. 线程和助手,技术上互相独立

这一节讲一个反直觉、但很有用的设计。

学生在书里问了个好问题:创建线程的时候我并没有指明用哪个助手, 是不是说明这两者是彼此独立的?

作者的回答是:逻辑上它们为了实现连续对话而相互关联, 而在技术实现上它们则是独立的组件9

这带来两个可能性,书都说了:

一个线程可以有多个助手。同一段对话里,天气问题路由给天气助手、 旅游问题路由给旅游助手——前提是你自己写那个路由逻辑

一个助手也可以有多个线程。同一个通用助手同时服务很多段对话, 它靠线程编号区分谁是谁

为什么这一节值得留着? 因为它解释了第 5 节那个状态图为什么是挂在「运行」上、 而不是挂在「助手」或「线程」上:运行才是那个「某个助手 × 某个线程 × 这一次」的交点。

4. 主走查:一次完整的托管调用

这一节是本章的主走查。每一步旁边的字串,全部来自书里那段真实代码和它的输出。

输入: 一条用户消息—— 「我把每束花定价为在进价的基础上加价20%,当进价为80元时,我的售价是多少。」10 助手: 名字叫「鲜花价格计算器」,指令是「你能够帮我计算鲜花的价格」, 开了一件内置工具:代码解释器11

第 1 步 · 造助手。 打印出来的对象里,名字、指令、模型、工具四样都在, 并且多了一个编号 asst_M7OR4XULWjFnXN9WSqJtQ0XR11

这里有个值得停一下的细节:为什么这么简单的加价计算还要开代码解释器? 书给了两个理由,第一个是早期的大模型被公认数学不好, 挂上代码解释器让它写代码来算比较稳妥12这和第 05 章第 7 节那个计算器工具是同一个手法:不让它算,让它写。

第 2 步 · 造线程。 返回一个编号 thread_ddl4SsbU9KlCdpxv7BfqVpQQ。 书里形容得很生动:从这时开始,线程将在后台一直运行13

第 3 步 · 往线程里加一条消息。 角色写 user,内容就是上面那句定价问题10注意:你只发了这一句。 前面几章那种「把整个数组重发一遍」的动作,这里没有。

第 4 步 · 开工。 指定线程和助手,创建一个运行。 打印出来的对象里,那个状态字段写着 status='queued'——排队中14

这一步还有一个字段值得看: created_at=1704989389expires_at=1704989989两个数相差 600 秒。 书说得很清楚:和线程那种「终止时间不确定的模糊感」不同, 运行有非常明确的起止时间15给个参照:600 秒就是 10 分钟。

第 5 步 · 再查一次。 调一次「取回」方法,状态变成 status='in_progress'——进行中16它还没算完,所以你得接着查。

第 6 步 · 轮询。 书里写了一个 while True 循环: 每 5 秒查一次状态,状态一旦变成完成、失败或过期就跳出17。实际打出来是三行:

Run Status: in_progress
Run Status: in_progress
Run Status: completed
Run completed successfully.

给个参照:间隔 5 秒查了 3 次,也就是这一趟大约花了 10 秒出头。

第 7 步 · 读回结果。 列出线程里所有消息,最新那一条就是助手加进去的。 它的内容是18:

在进价80元的基础上加20%,售价是96元。

核一下这个数:80 × 1.2 = 96。算对了。

第 8 步 · 拿它去用。 书里给的后续建议是:如果解析这个输出、 比如用一点小技巧把「96」这个数字读出来,就可以接着往下写程序了, 比如存进数据库19作者顺手提了一句更好的做法:直接让它输出 JSON。

造助手 ─┐
├─▶ 造运行 ──▶ queued ──▶ in_progress ──▶ completed ──▶ 读回「96 元」
造线程 ─┘ ▲ │
│ └──── 你每 5 秒问一次 ─┘
└─▶ 加消息

图说:这一趟从头到尾,你写的代码只有「造」和「问」两种动作。
真正的活是在对方那边干的,你连消息数组都没拼过。

5. 运行有一张状态图,而且你得自己转圈问

这一节把上一节第 4 到 6 步背后的机制讲清。

书给了一张完整的状态流转20。骨架是这样:

queued ──▶ in_progress ──▶ completed (成功)
│ │
│ ├──▶ failed (失败)
│ ├──▶ cancelling ──▶ cancelled (你主动取消)
│ └──▶ requires_action (★ 第 6 节讲这个)
│ │
└──────────────────────┴──▶ expired (超过那 10 分钟)

图说:六个状态里,只有两个是「你什么都不用做」——
排队中和进行中。其余四个都要你出手。

这一节真正要你记住的是那个「你得自己转圈问」。 接口不会等你,也不会事后主动回头来找你。 这种「对方干完了主动来找你」的做法叫通知,而这套接口没有提供。

创建运行这个调用是立刻返回的,返回的时候它还在排队。 想知道好没好,只能隔一会儿问一次。

这个「隔一会儿问一次」的做法叫轮询——定期主动去查一次状态, 直到它变成你要的那个值。书里那段代码就是一个纯粹的轮询循环。

为什么这一条不能跳过? 因为下一节那个坑,坑的正是这个循环的跳出条件。

6. 另起一处:漏掉一步,轮询了 101 次还没停

这一节走书里另一趟真实记录。它是全书最有教学价值的一次翻车。

输入(与主走查不同的一趟): 一条用户消息——「你好,请你安慰一下伤心的小雪吧!」 助手: 名字叫「鼓励 Agent」,指令是「你是一个很会鼓励人的助手!」, 挂了一个自定义函数 get_encouragement,说明写着「根据用户的心情提供鼓励信息」21

先看正常的那一趟。 同一个助手,先问了一句「你好,请和我随便说句话吧!」。 轮询三次就完了:排队 → 进行中 → 完成,拿到一段热情的回话22一切正常。

再看出事的那一趟。 只把那句话换成「安慰一下伤心的小雪」,其余代码一个字没改。 结果第 2 次轮询时,状态变成了一个新东西23:

status = 'requires_action'
required_action = 提交工具产出,内容是:
id = 'call_Q5B63L1bIseH3ZfUtpKhz9nQ'
function = get_encouragement
arguments = {"mood":"伤心","name":"小雪"}

看出来了吗? 这就是第 04 章那张调用单,只是换了个位置—— 它不在返回值里,而是挂在运行对象的一个字段上,而运行本身停下来等你。

然后程序就再也没有出去过。 书里那段轮询循环的跳出条件只写了「完成」, 于是它一直查、一直查。输出里赫然印着:Run的第101次轮询信息:24。 作者的原话是:程序进入无限循环,一直卡在等待函数调用状态,我只好按 Ctrl+C 强行退出。

给这个 101 配个参照:主走查那一趟只查了 3 次。 按同样 5 秒一次算,101 次就是 8 分多钟——已经很接近那 10 分钟的过期线了。

这个坑的根子是什么? 书里学生自己说破了:程序里没有调用那个函数、 也没有提交返回结果的逻辑,所以运行就一直处在等待状态,给不出回话25

所以这一趟的修法有两处,缺一不可:

第一,轮询的跳出条件要加上这个状态——不能只在完成时跳出26

第二,跳出之后要真的把结果交回去。下一节讲。

7. 交回结果之后,它回到排队,再跑一段

这一节讲那个必须做、而且有时限的收尾动作。

跳出循环之后,你要从运行对象上取三样东西:函数名、参数、以及那个调用编号27这三样和第 04 章那张调用单上的三样一模一样。

取到之后,在你自己的代码里真的把函数跑一遍,拿到那句鼓励的话。 然后调一个专门的方法把结果交回去,而且必须带上那个调用编号—— 书特意提醒:要确保编号准确对应,以便把响应结果和这次调用匹配上28

这一步有时限。 书写得很清楚:运行的生存周期大约 10 分钟, 不要让它等太久,否则会进入过期状态,那时它就收不到你的结果了29

交回去之后发生了一件很有意思的事:运行的状态回到了排队30

queued ─▶ in_progress ─▶ requires_action ─▶ (你交回结果) ─▶ queued ─▶ in_progress ─▶ completed

注意这里:它回到了起点那个状态

图说:一次带工具的运行,状态要走七格。
中间那个「你交回结果」是唯一一格由你完成的动作。

再轮询三次,状态变成完成,读回最终那段话——一段完整的、带着上下文的安慰31

把这一趟和第 04 章那一趟并排看,你会发现它们是同一件事的两种包装:

同一件事自己管那一套(第 04 章)托管这一套(本章)
模型说要点工具结束原因变成 tool_calls运行状态变成 requires_action
调用单在哪在返回值的 tool_calls挂在运行对象的一个字段上
结果怎么交回追加几条 tool 角色的消息调一个专门的方法提交
交回之后你自己再调一次模型运行自己回到排队,继续跑
漏了会怎样你的循环自然结束,拿不到答案运行挂在那儿,你的循环永远转下去

最后一行是这两套的真正差别。 自己管那一套,漏一步就是少一次调用; 托管这一套,漏一步是一个会烧掉你 10 分钟和无数次查询的活挂起。

8. 这套接口今天是什么状态

这一节交代口径,并且照实说我们核到了什么、没核到什么。

书自己就写明了这套东西当时还没定型:调用时要带一个标着 Beta 的标头, 作者解释说这表示服务仍处于公开测试阶段——可能已经相对稳定, 但厂商仍在收集反馈、准备进一步调整或增加功能32

今天的状态,我们能核到的只有一件事:官方主推已经换成了另一套。

补充(不在书里,依据我们的前沿框架书架): 那家厂商今天有一套官方的 agent 开发库,做的是同一件事—— 配好指令和工具的角色、一个把它跑到底的引擎、以及一套会话记忆。 依据: shelf=ai-frontier-reference/openai-agents-python#04-sessions-tracing-hitl.md @2c5560339cd7f77b4dabcf7d85c5d150594fd74c 事实=会话记忆被抽象成一个只有四个方法的协议(memory/session.py:16), 内置一个基于本地数据库的实现; 并且和「让服务端管历史」互斥——用了服务端那条路,本地就不再重复存 (run.py:679)。33

补充(不在书里,依据我们的前沿框架书架): 与此同时,这套托管接口本身仍然被别的框架在用。 依据: shelf=ai-frontier-reference/semantic-kernel#04-agents-and-threads.md @5e1f1fb87d9a38ed44683228dda2434a9b2f0ef9 事实=该框架里有一类角色对象直接调 client.beta.threads.create 建线程 (agents/open_ai/openai_assistant_agent.py:171-175), 也就是说本章讲的那条 beta 通道到今天仍然通着。34

判断(我们的,不是书里的): 官方主推已经换到另一套,这一条我们核到了。 但「这套托管接口已被正式宣布弃用」这个说法,我们在三个书架上都没有核到, 所以本组拆解不写弃用、也不写日期。 你今天要做决定,应该去看厂商自己的公告,而不是信这一段。 如果错,会错在: 如果厂商其实早已发过弃用公告、只是没有出现在我们的书架里, 那么这一段就把一件已经发生的事写成了「没核到」。

不管弃用与否,本章的机制都不会白学: 第 7 节那张对照表说明,requires_actiontool_calls 是同一件事的两种形状。 换成任何一家的托管接口,你都要在这一格上写同样的代码。

9. 可带走的

  1. 这一套的分界线只有一条:上下文不归你管了。 好处是不用拼消息数组,代价是你控制不了它花多少钱——这是书自己说的;
  2. **四个对象各管一段:**助手是「谁来干」、线程是「这一摊事」、消息是「这一句」、运行是「开工」;
  3. 线程不是程序里那个多线程,它就是一次对话;
  4. 线程和助手技术上独立,所以一个线程能换助手、一个助手能带很多线程;
  5. 运行是立刻返回的,状态先是排队。 想知道好没好,只能自己隔几秒问一次,这叫轮询;
  6. 运行有 10 分钟的生存期(书里那次是 created_atexpires_at 差 600 秒),过期就收不到你的结果了;
  7. **第五个状态 requires_action 是最大的坑:**它停在那儿等你把工具结果交回去;
  8. 轮询的跳出条件只写「完成」,程序就会永远转下去——书里真的转了 101 次才被手动掐掉;
  9. 交回结果时必须带上那个调用编号,交完之后运行回到排队、继续跑完;
  10. 它和第 04 章是同一件事的两种包装: requires_actiontool_calls,只是漏一步的后果严重得多;
  11. 这套接口今天什么状态,我们只核到「官方主推已换成另一套」;弃用的具体状态没核到,不写

10. 原文地图

主题原书章原文位置
它有点 agent 的意思4.1 OpenAI公司的Assistants是什么text/30-ch04-01-4-1-openai-assistants.txt:7(搜「有点Agent的意思」)
上下文不归你管、代价是控制不了成本4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:7(搜「无须管理对话线程和上下文内容」) · text/32-ch04-03-4-3-assistants-api.txt:225(搜「无法有效控制运行助手的成本」)
四步流程与助手的五样配置4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:11(搜「通过定义指令并选择模型来创建」) · text/32-ch04-03-4-3-assistants-api.txt:51(搜「name:助手的名称」)
线程是什么、别重复创建4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:168(搜「一个线程就代表和OpenAI公司大模型的一次对话」) · text/32-ch04-03-4-3-assistants-api.txt:103(搜「请勿重复运行创建助手的代码」) · text/32-ch04-03-4-3-assistants-api.txt:187(搜「不要一直重复运行创建线程的代码」)
线程和助手互相独立4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:203(搜「它们则是独立的组件」) · text/32-ch04-03-4-3-assistants-api.txt:211(搜「一个线程可以有多个助手」)
主走查:助手、消息、状态、964.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:71(搜「鲜花价格计算器」) · text/32-ch04-03-4-3-assistants-api.txt:233(搜「进价的基础上加价20」) · text/32-ch04-03-4-3-assistants-api.txt:314(搜「status='queued'」) · text/32-ch04-03-4-3-assistants-api.txt:348(搜「status='in_progress'」) · text/32-ch04-03-4-3-assistants-api.txt:441(搜「售价是96元」)
为什么开代码解释器4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:95(搜「早期的大模型被公认数学不好」)
运行的起止时间、轮询循环4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:321(搜「非常明确的起止时间」) · text/32-ch04-03-4-3-assistants-api.txt:388(搜「polling_interval」) · text/32-ch04-03-4-3-assistants-api.txt:412(搜「Run Status: in_progress」)
状态流转与那张状态表4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:359(搜「Run的生命周期始于queued」) · text/32-ch04-03-4-3-assistants-api.txt:373(搜「需要特别注意requires_action状态」)
读出 96 之后怎么用4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:485(搜「读取“96”这个数字」)
另起一处:鼓励助手、对照组、101 次轮询5.3 通过Assistants API实现Function Callingtext/38-ch05-03-5-3-assistants-api-function-calling.txt:40(搜「根据用户的心情提供鼓励信息」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:136(搜「今天过得怎么样」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:221(搜「requires_action」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:224(搜「第101次轮询信息」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:230(搜「强行退出」)
修法:改跳出条件、取三样、交回、10 分钟时限5.3 通过Assistants API实现Function Callingtext/38-ch05-03-5-3-assistants-api-function-calling.txt:244(搜「不能只在Run的状态为completed时才结束循环」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:278(搜「function_name = run.required_action」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:378(搜「确保tool_call_id准确引用每个function_id」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:376(搜「生存周期大约10min」) · text/38-ch05-03-5-3-assistants-api-function-calling.txt:399(搜「此时Run回到queued状态」)
Beta 标头4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:45(搜「OpenAI-Beta: assistants=v2」)

Footnotes

  1. 出处:「4.1 OpenAI公司的Assistants是什么」第 7 段(text/30-ch04-01-4-1-openai-assistants.txt:7,搜「有点Agent的意思」)。这一节前半段基本是照抄官方说明,作者自己也用「听了这么多『官话』」来形容(text/30-ch04-01-4-1-openai-assistants.txt:23,搜「官话」)。

  2. 出处:「4.3 Assistants API的简单示例」第 7 段(text/32-ch04-03-4-3-assistants-api.txt:7,搜「无须管理对话线程和上下文内容」)。同段还说明当时支持三类工具:代码解释器、检索和函数调用。

  3. 出处:「4.3 Assistants API的简单示例」第 225 段(text/32-ch04-03-4-3-assistants-api.txt:225,搜「无法有效控制运行助手的成本」)。这段话被排在一个不起眼的小框里,但它是这一整章最重要的一句判断——它把这套接口的全部优点和全部风险放进了同一句话。

  4. 出处:「4.3 Assistants API的简单示例」第 11 段起(text/32-ch04-03-4-3-assistants-api.txt:11,搜「通过定义指令并选择模型来创建」)。四步依次是:创建助手、创建线程、往线程加消息、在线程上运行助手。

  5. 出处:「4.3 Assistants API的简单示例」第 51 段起(text/32-ch04-03-4-3-assistants-api.txt:51,搜「name:助手的名称」)。原文还提醒:若要用检索工具,只能选较新的模型。

  6. 出处:「4.3 Assistants API的简单示例」第 168 段(text/32-ch04-03-4-3-assistants-api.txt:168,搜「一个线程就代表和OpenAI公司大模型的一次对话」)。

  7. 出处:「4.3 Assistants API的简单示例」第 103 段(text/32-ch04-03-4-3-assistants-api.txt:103,搜「请勿重复运行创建助手的代码」)。作者说自己就是在不明真相的情况下生成了一系列功能重复、编号不同的助手。

  8. 出处:「4.3 Assistants API的简单示例」第 187 段(text/32-ch04-03-4-3-assistants-api.txt:187,搜「不要一直重复运行创建线程的代码」)。作者接着记了一段很有意思的经历:他为此去官方论坛发帖问「删掉助手时线程会不会跟着清理」,得到的回复是**「他也不是 100% 确定」**,只知道 60 天没有动静的线程会被清理(text/32-ch04-03-4-3-assistants-api.txt:193,搜「60天内没有动静」)。这属于书里坦白说「不知道」的地方,照实记下来。

  9. 出处:「4.3 Assistants API的简单示例」第 203 段(text/32-ch04-03-4-3-assistants-api.txt:203,搜「它们则是独立的组件」)与第 211 段(text/32-ch04-03-4-3-assistants-api.txt:211,搜「一个线程可以有多个助手」)。作者在后一段用了「理论上是这样的」,说明他自己没有实测过。

  10. 出处:「4.3 Assistants API的简单示例」第 233 段(text/32-ch04-03-4-3-assistants-api.txt:233,搜「进价的基础上加价20」)。注意书里这句话在两处的印法略有出入:代码里写「加价20%」,而后面命令行输出里写「加20%」,意思相同。 2

  11. 出处:「4.3 Assistants API的简单示例」第 71 段(text/32-ch04-03-4-3-assistants-api.txt:71,搜「鲜花价格计算器」)。打印出来的对象里编号、创建时间戳、指令、模型、工具五样齐全。 2

  12. 出处:「4.3 Assistants API的简单示例」第 95 段(text/32-ch04-03-4-3-assistants-api.txt:95,搜「早期的大模型被公认数学不好」)。作者给的第二个理由很实在:「我想在这里向你展示如何指定工具」——也就是说这件工具一半是为了教学。

  13. 出处:「4.3 Assistants API的简单示例」第 183 段(text/32-ch04-03-4-3-assistants-api.txt:183,搜「一直在侦听你和它对话进展的探子」)。原文的比喻是「派出了一个一直在侦听你和它对话进展的探子」。

  14. 出处:「4.3 Assistants API的简单示例」第 314 段(text/32-ch04-03-4-3-assistants-api.txt:314,搜「status='queued'」)。书特意提醒:针对这个对象最需要注意的就是状态字段(text/32-ch04-03-4-3-assistants-api.txt:319,搜「最需要注意的是status」)。

  15. 出处:「4.3 Assistants API的简单示例」第 321 段(text/32-ch04-03-4-3-assistants-api.txt:321,搜「非常明确的起止时间」)。「600 秒就是 10 分钟」这个折算是我们做的,书里只给了两个时间戳;不过原书第 5 章后面明确说过生存周期大约 10 分钟,两处对得上。

  16. 出处:「4.3 Assistants API的简单示例」第 348 段(text/32-ch04-03-4-3-assistants-api.txt:348,搜「status='in_progress'」)。

  17. 出处:「4.3 Assistants API的简单示例」第 388 段(text/32-ch04-03-4-3-assistants-api.txt:388,搜「polling_interval」)与第 412 段起(text/32-ch04-03-4-3-assistants-api.txt:412,搜「Run Status: in_progress」)。「大约 10 秒出头」是我们按 5 秒 × 3 次算的,书里没有给耗时。

  18. 出处:「4.3 Assistants API的简单示例」第 441 段(text/32-ch04-03-4-3-assistants-api.txt:441,搜「售价是96元」)。这条消息的角色是 assistant,而且带着产生它的那次运行的编号——同一个线程里的消息未必属于同一次运行,书在末尾专门记了这个观察(text/32-ch04-03-4-3-assistants-api.txt:489,搜「未必属于同一个Run」)。

  19. 出处:「4.3 Assistants API的简单示例」第 485 段(text/32-ch04-03-4-3-assistants-api.txt:485,搜「读取“96”这个数字」)。作者把「怎么让它直接输出 JSON」留成了一道思考题,原文写着「至于如何实现到,你可以自己思考」。

  20. 出处:「4.3 Assistants API的简单示例」第 359 段起(text/32-ch04-03-4-3-assistants-api.txt:359,搜「Run的生命周期始于queued」)。书里还配了一张完整的状态说明表;本节那张图是我们按正文那几段文字画的,书里的原图是截图。

  21. 出处:「5.3 通过Assistants API实现Function Calling」第 40 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:40,搜「根据用户的心情提供鼓励信息」)。这个助手是第 5.2 节在网页界面上手动建的,这里只是按编号取回来用。

  22. 出处:「5.3 通过Assistants API实现Function Calling」第 136 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:136,搜「今天过得怎么样」)。这一趟的用量数字书里也给了:输入 348、输出 154、合计 502(text/38-ch05-03-5-3-assistants-api-function-calling.txt:133,搜「completion_tokens=154」)。

  23. 出处:「5.3 通过Assistants API实现Function Calling」第 221 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:221,搜「requires_action」)。参数那一格里的中文是模型自己从「伤心的小雪」这句话里抽出来的——心情填「伤心」,名字填「小雪」,一个字没错。

  24. 出处:「5.3 通过Assistants API实现Function Calling」第 224 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:224,搜「第101次轮询信息」)与第 230 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:230,搜「强行退出」)。「8 分多钟」是我们按 5 秒 × 101 次算的,书里没有给耗时。

  25. 出处:「5.3 通过Assistants API实现Function Calling」第 242 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:242,搜「无法给出情感支持的对话内容」)。这段是学生自己想通之后说的,作者没有再补充。

  26. 出处:「5.3 通过Assistants API实现Function Calling」第 244 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:244,搜「不能只在Run的状态为completed时才结束循环」)。改完之后的循环在同节第 262 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:262,搜「if run.status in」)。

  27. 出处:「5.3 通过Assistants API实现Function Calling」第 278 段起(text/38-ch05-03-5-3-assistants-api-function-calling.txt:278,搜「function_name = run.required_action」)。取出来的三样分别打印在同节第 287 段起(text/38-ch05-03-5-3-assistants-api-function-calling.txt:287,搜「function_name: get_encouragement」)。

  28. 出处:「5.3 通过Assistants API实现Function Calling」第 378 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:378,搜「确保tool_call_id准确引用每个function_id」)。这句话和第 04 章第 5 节那条要求一字之差,做的是同一件事。

  29. 出处:「5.3 通过Assistants API实现Function Calling」第 376 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:376,搜「生存周期大约10min」)。原文的措辞是「因为 Run 有时效性……应及时提交调用结果,否则 Run 就会进入 expired 状态,这样的话它将接收不到结果」。

  30. 出处:「5.3 通过Assistants API实现Function Calling」第 399 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:399,搜「此时Run回到queued状态」)。

  31. 出处:「5.3 通过Assistants API实现Function Calling」第 437 段(text/38-ch05-03-5-3-assistants-api-function-calling.txt:437,搜「你都不是孤独一人」)。这一趟的用量:输入 760、输出 214、合计 974(text/38-ch05-03-5-3-assistants-api-function-calling.txt:417,搜「total_tokens=974」)——给个参照:上面那一趟正常对话是 502,带了一次工具调用之后接近翻倍。

  32. 出处:「4.3 Assistants API的简单示例」第 45 段(text/32-ch04-03-4-3-assistants-api.txt:45,搜「OpenAI-Beta: assistants=v2」)。原文还说明:用官方的两种语言工具包时不必自己管这个标头,系统会自动处理。

  33. 补充(不在书里,依据我们的前沿框架书架):那家厂商今天的官方 agent 开发库把「历史存哪」做成了一个极薄的协议。依据: shelf=ai-frontier-reference/openai-agents-python#04-sessions-tracing-hitl.md @2c5560339cd7f77b4dabcf7d85c5d150594fd74c 事实=会话协议只要求四个方法(memory/session.py:16),内置一个基于本地数据库的实现,另有多种可选实现;且本地持久化与「让服务端管历史」互斥(run.py:679)。这一条只能证明官方主推的写法变了,不能证明本章这套接口的状态。

  34. 补充(不在书里,依据我们的前沿框架书架):本章这条通道到今天仍被别的框架调用。依据: shelf=ai-frontier-reference/semantic-kernel#04-agents-and-threads.md @5e1f1fb87d9a38ed44683228dda2434a9b2f0ef9 事实=该框架里有一类角色对象在创建线程时直接调 client.beta.threads.create(agents/open_ai/openai_assistant_agent.py:171-175),线程编号存在服务端。这一条只能证明「接口还在」,不能证明「没被弃用」。