跳到主要内容

一次调用里到底有什么 — 发过去的、收回来的、和那张账单

这一章讲三件事: 「问一次模型」这个动作发过去的是什么、收回来的是什么; 为什么它什么都不记得;以及那个「为什么停下来」的字段为什么是全书最重要的一个字段。

它在全书链条里的位置: 第 01 章那一圈里,第①③⑤步全是「问模型」。 这一章就是把那一格拆开。 第 04、05、06 三章要用到的每一个零件,都在这一章里第一次露面。

为什么这一章换了输入。 第 01 章那趟鲜花定价的记录里没有账单数字。 而讲这一层绕不开「一次调用花了多少」,所以这一章改用书里唯一一次 把用量数字给全了的调用——一段四条消息的鲜花对话。

1. 主走查:一次真实调用,从发出去到读回来

这一节是本章的主走查。下面每一个数字和字串,全部来自书里那次调用打印出来的真实结果。

输入: 一段四条消息的鲜花对话(四条的原文在第 2 节), 外加一项设置:要求它按一种固定格式回答。

第 1 步 · 发出去。 发过去的不是一句问话,是四条消息排成一排, 外加几样设置(用哪个模型、输出要什么格式)。第 2 节把这四条逐条拆开。

第 2 步 · 收回来。 回来的不是一段文字,是一个对象; 里面至少有三处必须认识:它写的那段内容、一处写着 finish_reason='stop'、 以及末尾挂着的三个数。

第 3 步 · 看它写的那段内容。 它真的按要求给了一段带层次的结构化文本: 外层是 "response""details""recommendation" 三项, "details" 下面还套了供应商、地点、订单时间等五个子项1第 5 节讲这是怎么要来的、以及会踩什么坑。

第 4 步 · 看 finish_reason='stop' 它的意思是「这一轮它自己说完了」。 第 6 节讲它还有哪两个取值,以及为什么它是全书最重要的一处。

第 5 步 · 看账单。 末尾那三个数是: 输入 89、输出 254、合计 3432下面把这三个数讲透。

这三个数的单位叫 token——模型把文字切碎之后的小块。 一个英文单词大约算一块多一点,中文一个字往往就要占一块甚至更多。 书给的换算是:1000 个 token 大约相当于 750 个英文单词3

给这三个数配个参照物,你才知道 343 是多是少。 书里说, 两百万个 token 大约能处理三千页文本4。 按这个比例折回去,343 个 token 大概就是半页纸—— 一次很短的问答。而这半页纸,输出那一半(254)几乎是输入(89)的三倍。

为什么输入输出要分开记? 因为两边的价钱不一样,输出通常贵得多。 而这一点会在第 05 章变成一件很要命的事: 那一圈每转一轮,整段历史都要作为输入重发一遍。

这就是这一趟走查的落点:你不是按次付钱,是按字数付钱,而且输入输出分开算。

2. 发过去的不是一句话,是一个消息数组

这一节讲清「你到底给它发了什么」。

书里那次调用发过去的不是一句问话,而是四条消息排成一排5。 四条依次是:

[0] system : "您是一个帮助用户了解鲜花信息的智能助手,并能够输出JSON格式的内容。"
[1] user : "生日送什么花最好?"
[2] assistant : "玫瑰花是生日礼物的热门选择。"
[3] user : "送货需要多长时间?"

图说:第 [2] 条是「助手说过的话」,可它是你自己写进去的 ——
这一趟对话在此之前根本没发生过。模型看到的只有这四行。

每条消息由两样东西组成:一个角色和一段内容。角色只有三种6:

角色干什么用在上面那四条里是哪一条
system定人设、定输出格式,整段对话的背景第 [0] 条
user用户说的话第 [1]、[3] 条
assistant助手说过的话(可以是真的,也可以是你编的)第 [2] 条

调用时还要一并交代几样设置,书里管这些叫参数—— 调用时可以拧的那些开关和取值,比如用哪个模型、输出要什么格式。

发这个请求的那个对象,书里的代码里一律叫 client,中文叫客户端—— 指发起请求的那一方;对面接住请求的那一方叫服务端。 书专门花了一段解释这个命名习惯:它就是「谁发请求」的那个位子7

而这一整套「你按什么格式发过去、它按什么格式还回来」的约定, 统称一个 API(中文叫接口)——两个程序之间约好的说话方式。 这本书从头到尾都在跟三套接口打交道。

3. 它什么都不记得,记忆是你每次重发的那一段

这一节的结论一句话:第 [2] 条「助手说过的话」是你自己塞进去的。

书里有一句对话点破了这件事。学生问:这个消息数组其实承载了它的「短期记忆」,对吗? 老师答:说得很好8

请把这句夸奖当成一条硬事实来读:所谓短期记忆,就是这个数组本身,没有别的东西。 模型那边不存任何东西。你第二次发请求时,它对第一次发生过什么一无所知—— 除非你把第一次的问答原样再抄一遍塞进数组里。

这就带出一个直接后果。把它拆成四级台阶:

  1. 它每次只是在续写一段文字;
  2. 所以「上一轮说过什么」不在它脑子里,只能写进这段文字;
  3. 于是每问一次,整段对话都要重发一遍;
  4. 重发的部分照样按第 1 节那个价钱计费、照样占时间。

对话越长越贵越慢——这就是全书所有「压缩历史」「只带最近几轮」的动机。

模型这一次能看到的全部文字,行话叫上下文——就是你这一轮发过去的那一整段

模型一次最多能读进多长的一段,是有上限的,这个上限叫上下文窗口。 书里没有正面讲它,只在原书第 4 章顺带提过一句:托管那一套会自己做压缩, 「确保对模型的请求适合最大上下文窗口」9这句话的言外之意是:超了就得砍,而砍掉哪一段你说了不算。 第 06 章会算这笔账。

4. 温度这个旋钮:取值 0 到 2,做这类程序拧到 0

这一节只讲机制。这个旋钮解决不了什么问题,第 03、07 两章各讲一层。

书说得很干脆:这是最常见的一个可拧项,用于控制生成内容的随机性和创造性; 值低(比如 0.2)输出更一致,值高(比如 1.0)输出更多样; 取值范围为 0 至 210

为什么它必须留名? 因为你合上这份文档去用任何一家的接口, 都会在文档里撞见 temperature 这个词。它就是这里说的这个旋钮。

书里所有做这类程序的示例,几乎都写着 temperature=0。 第 06、07、09、11 章的代码里全是这一句。理由很朴素:同一个输入,尽量给同一个输出。

但这个旋钮管不了两件事,现在先记下,后面各有一节:

  • 拧到 0 也不能让一趟任务的步数固定下来——那是第 03 章第 8 节讲的;
  • 更管不了「五步接力、每一步的偏差被下一步继承」——那是第 07 章第 9 节讲的。

5. 要它吐结构化数据:发过去的文字里必须出现「JSON」这四个字母

这一节讲一个书里明写、但很容易踩的坑。

先说这个格式是什么。JSON一种轻量的数据写法,用大括号和冒号把「名字:值」一层层套起来, 人能读、机器也容易拆开。书给的定义是「一种轻量级数据交换格式」11

书里那次调用特意加了一项设置,要求返回 JSON; 结果确实回来一段带 "response""details""recommendation" 三层的结构化文本1

坑在哪? 原书第 5 章里补了一条。原话里有个词叫字符串——一串连在一起的文字

原话是:如果在上下文中没有出现「JSON」这个字符串,接口将反馈出错信息12。 也就是说,你光在设置里打开这个开关不够,发过去的那几条消息里还必须真的出现这四个字母。 回头看第 2 节那条 system 消息——它末尾那句「并能够输出JSON格式的内容」不是随手写的。

书还给了这个开关的边界,一句话:它保证输出是有效且无错误的 JSON 对象, 但并不保证输出结果能匹配任何特定的架构或格式13这句话要划重点:合法 ≠ 是你要的那个形状。 第 04 章讲工具接入时,这条会再出现一次。

6. 回复里有个「为什么停下来」的字段

这一节介绍全书最重要的一个字段。它现在看起来毫不起眼,四章之后你会天天用它。

先说字段是什么:一份结构化数据里的一个具名的格子, 比如上面 JSON 里那个 "response" 就是一个字段。

书里那次调用的返回对象很长,但有一处必须现在就认识:finish_reason='stop'14。 它的意思是「这一轮它自己说完了」。

为什么它要紧? 因为它还有别的取值,而每一个取值都对应后面一整章:

取值意思哪一章处理它
stop说完了,这就是答案本章这次调用
tool_calls它要点工具,还没答第 04 章
length被输出上限截断了,内容可能不完整第 04 章第 8 节

书在讲 JSON 那一段特意提醒:达到输出上限时返回的结构可能是不完整的, 读取之前需要先检查这个字段15换句话说:不看这个字段就直接去拆返回值,是一个会安静出错的写法。

7. 换一层皮:把交代、模型、解析器串成三段

这一节回答:既然接口这么直白,为什么书还要引入一个框架?

书里介绍的框架是 LangChain,一个开源(指源代码公开、任何人都能看和改)项目, 2022 年 10 月启动,比那个引爆一切的聊天产品还早一个月16

它给的第一个好处是换模型不改代码。书里演示了一个「模型实验室」: 同一个问题「百合花源自哪个国家?」同时问三家模型,把三份答案并排打出来。 作者的评语是其中一家答得最好、另一家答案不正确、还有一家只是把问题复述了一遍17

它给的第二个好处是一种叫 LCEL 的写法:用一个竖线把几段拼起来。 书里那行代码是:

chain = prompt | model | output_parser

三段各干一件事18:

第一段是提示词——你交给模型的那段文字,包括问题本身和一切给它的交代; 这里它是一个带占位符的模板,比如「请讲一个关于 {topic} 的故事」,调用时把 {topic} 换成「水仙花」。

第二段是模型本身,负责把这段文字往下续写。

第三段负责解析——把模型吐出来的那坨东西,按你要的形状拆开、取出有用的部分; 这里它只做最简单的一件事:确保拿到的是一个字符串。

{"topic": "水仙花"}


[prompt] ── "请讲一个关于 水仙花 的故事" ──▶ [model] ── 一大段故事文字 ──▶ [parser] ──▶ 字符串
│ │ │
填空 续写 取出来

图说:竖线读作「把左边的产出交给右边」。
第 03 章那段 ReAct 提示词,就装在最左边这一格里。

为什么值得学这个写法? 因为第 03 章之后所有的示例都长这个样子。 不认识这根竖线,后面每一段代码你都会卡住。

8. 这个框架有六个模块,而且互相不管

这一节给一张地图,后面十章会不断回到它。

书把这个框架切成六块19其中三块前面已经见过了: 模型 I/O 就是第 7 节那三段串起来的东西,Agents 就是第 01 章那一圈, 记忆就是第 3 节说的那个消息数组。

第四块叫,指的是把几步固定地串成一条流水线——第 7 节那根竖线拼出来的就是一条链。

第五块叫检索——去你自己的资料里搜一遍,把搜到的贴进问题里再问模型; 第 08、10 两章整章讲它。

第六块叫回调——在流程的每一步上挂一个函数,让它把中间过程报出来; 它负责记录和传输链的中间步骤,第 13 章讲「怎么看它到底干了什么」时会用到。

书特意点了一句这六块的关系:它们的耦合非常松散,各组件之间没有调用顺序, 也没有固定的接口,开发者可以自由设计并组合20这句话既是优点也是代价——自由度高,同时意味着没有一条标准路线可抄。

9. 边界:这一层还有三件事书里提了但没展开

这一节把书里点到为止的三件事收在一处,免得你以为漏读了。

第一,速率上限。 短时间发太多请求会被挡回来,报错里会带一个「多久之后再试」的时间21。 书给的建议是「尽可能在一个请求中获取所需的所有信息」—— 这条建议和第 01 章那一圈天然打架:那一圈本来就要发很多次请求。 书没有正面处理这个矛盾。

第二,数据保留。 书里写的口径是:自 2023 年 3 月 1 日起保留通过接口传输的数据 30 天, 但不再用这些数据改进模型22这是 2024 年初的说法,今天要以对方的最新政策为准。

第三,这个框架自己的毛病。 书列了三条:生态太复杂、数据量大时效率会成问题、 以及版本迭代——一版接一版地改——速度非常快,旧的代码在新版本中可能无法正常运行23最后这一条在今天已经兑现了——第 05 章第 9 节和第 13 章第 3 节会具体到行。

10. 可带走的

  1. 一次调用发过去的是一排消息,不是一句话; 每条消息 = 一个角色 + 一段内容,角色只有三种;
  2. assistant 那条「它说过的话」可以是你编的——模型分不出真假,它只读这排文字;
  3. 模型不存任何东西。 所谓短期记忆就是这排消息本身,每轮都要整个重发;
  4. 按 token 计费,输入输出分开算,输出更贵。 书里那次短对话是 89 进 254 出、合计 343,大约半页纸;
  5. 对话越长越贵越慢,因为每一轮都把整段历史重发一遍——这是后面所有「压历史」手段的动机;
  6. temperature 是那个管随机性的旋钮,取值 0 到 2,做这类程序一律拧到 0;但它管不了步数,也管不了多步接力的偏差;
  7. 要 JSON 格式,消息里必须真的出现「JSON」这四个字母,否则接口直接报错;
  8. 它只保证是合法 JSON,不保证符合你要的结构——这两件事差得很远;
  9. finish_reason 这个字段决定了下一步该干什么: stop 是答完了、tool_calls 是要点工具、length 是被截断了;
  10. 一根竖线把提示词、模型、解析器串成三段——后面所有示例都长这个样子;
  11. 六个模块互相不管,没有固定顺序:自由度是它的卖点,也是它没有标准路线可抄的原因。

11. 原文地图

主题原书章原文位置
那次调用的四条消息3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:148(搜「您是一个帮助用户了解鲜花信息的智能助手」) · text/25-ch03-01-3-1-openai-api.txt:151(搜「送货需要多长时间」)
三个用量数字3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:221(搜「completion_tokens=254」) · text/25-ch03-01-3-1-openai-api.txt:222(搜「prompt_tokens=89」) · text/25-ch03-01-3-1-openai-api.txt:223(搜「total_tokens=343」)
token 是什么、怎么换算3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:319(搜「什么是Token」) · text/25-ch03-01-3-1-openai-api.txt:329(搜「3000页文本」)
三种角色3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:169(搜「每条消息包含一个角色」) · text/25-ch03-01-3-1-openai-api.txt:171(搜「代表系统级的指令」)
客户端这个命名3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:137(搜「客户端-服务器模型」)
消息数组就是短期记忆3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:179(搜「短期记忆」)
上下文窗口4.3 Assistants API的简单示例text/32-ch04-03-4-3-assistants-api.txt:225(搜「最大上下文窗口」)
温度旋钮3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:295(搜「取值范围为0至2」)
JSON 是什么、JSON 模式5.1 OpenAI中的Functionstext/36-ch05-01-5-1-openai-functions.txt:75(搜「轻量级数据交换格式」)
返回的 JSON 内容、JSON 模式说明3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:209(搜「送货时间取决于多个因素」) · text/25-ch03-01-3-1-openai-api.txt:189(搜「JSON模式」)
上下文里必须有「JSON」、不保证结构5.4 通过ChatCompletion API来实现Tool Callstext/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:154(搜「API将反馈出错信息」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:156(搜「需要检查返回的 finish_reason」) · text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:158(搜「并不保证输出结果能匹配任何特定的架构」)
结束原因字段3.1 何谓OpenAI APItext/25-ch03-01-3-1-openai-api.txt:205(搜「finish_reason='stop'」)
框架的起源、三家模型对比3.2 何谓LangChaintext/26-ch03-02-3-2-langchain.txt:13(搜「2022年10月」) · text/26-ch03-02-3-2-langchain.txt:63(搜「Cohere的答案不正确」)
三段链与竖线3.2 何谓LangChaintext/26-ch03-02-3-2-langchain.txt:119(搜「chain = prompt
六大模块、松耦合3.2 何谓LangChaintext/26-ch03-02-3-2-langchain.txt:151(搜「模型I/O」) · text/26-ch03-02-3-2-langchain.txt:175(搜「耦合非常松散」)
速率限制、数据保留、框架的三条毛病3.1 何谓OpenAI API 与 3.2 何谓LangChaintext/25-ch03-01-3-1-openai-api.txt:309(搜「速率被限制」) · text/25-ch03-01-3-1-openai-api.txt:303(搜「数据30天」) · text/26-ch03-02-3-2-langchain.txt:83(搜「版本迭代速度非常快」)

Footnotes

  1. 出处:「3.1 何谓OpenAI API」第 209 段(text/25-ch03-01-3-1-openai-api.txt:209,搜「送货时间取决于多个因素」)与第 189 段(text/25-ch03-01-3-1-openai-api.txt:189,搜「JSON模式」)。返回的那段结构里,「details」下面还套了五个子项(供应商、地点、订单时间、配送服务、特殊节日)。 2

  2. 出处:「3.1 何谓OpenAI API」第 221、222、223 段(text/25-ch03-01-3-1-openai-api.txt:221,搜「completion_tokens=254」;text/25-ch03-01-3-1-openai-api.txt:222,搜「prompt_tokens=89」;text/25-ch03-01-3-1-openai-api.txt:223,搜「total_tokens=343」)。这三个数印在返回对象的末尾,书里逐条解释过每个数是什么。

  3. 出处:「3.1 何谓OpenAI API」第 319 段(text/25-ch03-01-3-1-openai-api.txt:319,搜「什么是Token」)。原文的直译是「令牌」,也叫「子词」;作者说模型就是靠把文字拆成一个个这样的小块来训练和推理的,所以计费也用它。

  4. 出处:「3.1 何谓OpenAI API」第 329 段(text/25-ch03-01-3-1-openai-api.txt:329,搜「3000页文本」)。原文还给了另一个参照:一整套莎士比亚作品大约 90 万个单词,也就是上百万个 token。「半页纸」这个折算是我们按书里给的两个比例算的,不是书里的数。

  5. 出处:「3.1 何谓OpenAI API」第 148 段(text/25-ch03-01-3-1-openai-api.txt:148,搜「您是一个帮助用户了解鲜花信息的智能助手」);同节第 151 段(text/25-ch03-01-3-1-openai-api.txt:151,搜「送货需要多长时间」)。四条消息原样印在书里,本节那张图只是把它们排整齐,内容一字未改。

  6. 出处:「3.1 何谓OpenAI API」第 169 段(text/25-ch03-01-3-1-openai-api.txt:169,搜「每条消息包含一个角色」)与第 171 段(text/25-ch03-01-3-1-openai-api.txt:171,搜「代表系统级的指令」)。原文对三种角色各给了一句说明和一个例子。

  7. 出处:「3.1 何谓OpenAI API」第 137 段(text/25-ch03-01-3-1-openai-api.txt:137,搜「客户端-服务器模型」)。原文专门用一段解释为什么示例代码里那个变量叫 client:在涉及网络请求的场合,发起请求的一方习惯上就叫这个名字。

  8. 出处:「3.1 何谓OpenAI API」第 179 段(text/25-ch03-01-3-1-openai-api.txt:179,搜「短期记忆」)。这是书里的师生对话体,学生提出这个判断,老师只回了一句「说得很好」——书没有把它写成正式结论,但全书后面的做法都建立在这条上。

  9. 出处:「4.3 Assistants API的简单示例」第 225 段(text/32-ch04-03-4-3-assistants-api.txt:225,搜「最大上下文窗口」)。原文说的是托管那一套会自己做优化(「例如 ChatGPT 会进行压缩」)来保证请求装得下,代价是你无法有效控制运行成本——这句原话在第 06 章会再引一次。

  10. 出处:「3.1 何谓OpenAI API」第 295 段(text/25-ch03-01-3-1-openai-api.txt:295,搜「取值范围为0至2」)。原文举的对照是:聊天机器人和客服类应用用低值保证一致,创意写作用高值激发新想法。

  11. 出处:「5.1 OpenAI中的Functions」第 75 段(text/36-ch05-01-5-1-openai-functions.txt:75,搜「轻量级数据交换格式」)。书是在讲工具接入时才正式定义这个格式的,本章提前把它借过来用。

  12. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 154 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:154,搜「API将反馈出错信息」)。原文还给出了不报错时的另一种坏结果:模型可能生成无限多的空白字符,一直跑到用光输出上限。

  13. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 158 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:158,搜「并不保证输出结果能匹配任何特定的架构」)。这一条是这本书对这个开关最清醒的一句话,第 04 章第 8 节会把它和另外三条注意一起完整给出。

  14. 出处:「3.1 何谓OpenAI API」第 205 段(text/25-ch03-01-3-1-openai-api.txt:205,搜「finish_reason='stop'」)。这个字段印在返回对象的第二层里;原书第 5 章讲工具接入时,同一个字段的值变成了 tool_calls(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:101,搜「表示对话结束的原因是需要执行工具调用」)。

  15. 出处:「5.4 通过ChatCompletion API来实现Tool Calls」第 156 段(text/39-ch05-04-5-4-chatcompletion-api-tool-calls.txt:156,搜「需要检查返回的 finish_reason」)。原文的说法是:达到输出上限或对话超出长度限制时,返回的结构可能不完整,解析前要先检查这个字段。

  16. 出处:「3.2 何谓LangChain」第 13 段(text/26-ch03-02-3-2-langchain.txt:13,搜「2022年10月」)。作者顺带调侃了一句:这个启动时间比那个引爆一切的聊天产品还早一个月,「他那时候是怎么发现大模型要火的?」

  17. 出处:「3.2 何谓LangChain」第 63 段(text/26-ch03-02-3-2-langchain.txt:63,搜「Cohere的答案不正确」)。三家模型的答案原文在书里是一张截图,正文只给了作者的评语;所以本节只复述评语,没有引具体答案。

  18. 出处:「3.2 何谓LangChain」第 119 段(text/26-ch03-02-3-2-langchain.txt:119,搜「chain = prompt | model | output_parser」)与第 139 段(text/26-ch03-02-3-2-langchain.txt:139,搜「符号连接不同组件的」)。书里那个例子问的是「请讲一个关于 水仙花 的故事」,并把模型写的整篇故事原样印了出来。

  19. 出处:「3.2 何谓LangChain」第 151 段起(text/26-ch03-02-3-2-langchain.txt:151,搜「模型I/O」)。六块依次是:模型 I/O、检索、Agents、链、记忆、回调;作者说前四块最重要,后两块被归到「附加组件」。

  20. 出处:「3.2 何谓LangChain」第 175 段(text/26-ch03-02-3-2-langchain.txt:175,搜「耦合非常松散」)。原文接着展示了一张系统架构图,把六块摆在一起——但那张图是别人画的,作者在图注里标了来源。

  21. 出处:「3.1 何谓OpenAI API」第 309 段(text/25-ch03-01-3-1-openai-api.txt:309,搜「速率被限制」)与第 313 段(text/25-ch03-01-3-1-openai-api.txt:313,搜「重试时间」)。原文解释这是为了防止服务器过载、保证所有用户公平访问。

  22. 出处:「3.1 何谓OpenAI API」第 303 段(text/25-ch03-01-3-1-openai-api.txt:303,搜「数据30天」)。这是书里写下的口径,时间点是 2024 年初春成书之前;这类政策会变,真要依赖它请以对方最新的数据使用政策为准。

  23. 出处:「3.2 何谓LangChain」第 83 段(text/26-ch03-02-3-2-langchain.txt:83,搜「版本迭代速度非常快」)。作者甚至补了一句很坦白的话:如果你权衡之后决定不用这个框架、直接调接口,「我也不觉得有任何不妥」。