第2章 模型与程序的信息交换——从一次请求到工具接续
让模型参与任务,程序需要反复向它提供当前要求、材料和观察,再接住它返回的判断或工具请求。第1章已经说明这条交互的运行责任,本章开始落实信息交换:一次要求怎样组织成消息,消息怎样成为模型请求,实际操作的结果又怎样进入下一次输入。
实践采用DeepSeek官方API和Python标准库。先接通一次纯文本请求,理解认证、输入与返回,再沿着调用标识建立工具往返。第3章会将这些关系与固定工具、持久记录和产物验收接成完整的摘录任务。完整首请求源码保留在本章,传输细节与验证记录另列,阅读主线始终是程序发出了什么、接住了什么,以及下一次判断需要什么。
2.1会话、用户轮次、消息与模型请求
程序组织连续交互时,首先需要一个逻辑容器。会话(conversation)关联相关的用户输入、模型输出和工具结果,应用可以保存这些记录,并在后续使用它们。一条用户消息和一条助手消息,就能组成一个最小会话。本章所选接口由程序准备每次发送的消息,会话的连续性因此落实在应用保存和提交的信息中;它并不依赖一个始终在线、自动记住全部历史的模型进程。
会话中,由一次用户发言发起的推进称为用户轮次(user turn,以下简称用户 turn)。程序收到这次发言后,为回应它进行的模型判断和工具往返都可以属于这一轮,直到系统答复、停止或进入明确等待状态。没有工具时,一条要求及其答复就可构成很小的用户 turn。用户随后纠正或追加要求,会开启新的用户 turn;新发言如何接入仍在进行的工作,由运行系统安排。
交互记录以消息(message)为结构化单元。消息通常带有角色和内容,用于表达谁在这次交换中提供了什么。例如,一条 user 消息承载要求,一条 assistant 消息承载模型返回。后者还可以包含调用请求,程序执行后的结果另以消息记录。界面上显示的聊天气泡只是呈现方式,一条结构化消息可能有多个字段,部分内容也未必适合直接展示。
这些对象的组织关系可以用固定摘录任务说明。下面是一份逻辑示意,并非真实运行轨迹:
会话 C
用户 turn U1
user:根据指定材料创建一个摘录文件
assistant:请求读取材料
tool:返回读取结果
assistant:请求创建文件
tool:返回创建结果
assistant:报告结果
用户 turn U2
user:把摘录文件的标题改短一些
……
U1 中,读取和创建是同一次要求之内的内部步骤。它们增加了消息和工具往返,用户仍只发起了一轮要求。U2 的标题修改则是新的发言,可以继续针对同一产物。由此可以看出,任务作为有目标和完成条件的一项工作,范围由业务决定:一项任务可以跨越多个用户 turn,一段会话也可以先后处理不同任务。
模型服务参与其中的单位是模型请求:程序提交一次输入并取得一次响应的接口往返。输入包括本次消息、模型名、输出上限和其他配置;提供工具时,还包括工具描述与参数结构。最小请求可以只提交一条用户消息并取得一条助手消息。较长的用户 turn 则可能包含多次请求,每次都有自己的输入、响应、用量和错误。
这个任务的逻辑过程可能对应三次模型请求。第一次选择读取材料,第二次在取得材料后提出创建请求,第三次在取得创建结果后形成答复。程序可以将它们分别记为 request_1、request_2、request_3,同时关联到同一任务和 U1。一次助手响应中也可能提出多个工具调用,因此请求数、工具调用数、消息数和用户 turn 数应分别记录。
本次输入是模型这一轮计算的依据。程序从保存的历史中选取有关消息,加入当前目标、材料与工具信息,形成请求上下文。没有提交的材料不会仅因会话相同而自动可见。历史再次提交时,还要保留它的含义:旧消息可以解释任务如何走到这里,当前目标、授权和材料版本则规定接下来怎样推进。用户撤回的授权不能继续按有效条件使用。
建设时可以分别回答四个问题:任务要得到什么,当前正在回应哪次发言,程序已经保存了哪些记录,模型这次实际看到了什么。任务状态、用户 turn、完整历史与请求载荷各自给出一部分答案。先把它们关联起来,后续工具调度和用量记账才不会把内部步骤误记成新任务。
2.2消息角色、内容类型与实际来源
读消息时,先辨认它在接口中的身份,再辨认它承载的内容。角色(role)表示消息在交互结构中的位置;本章接口常用四种角色:
| 角色 | 消息承担的用途 |
|---|---|
system | 程序配置的基础指令 |
user | 用户输入,或按接口封装的任务内容 |
assistant | 模型返回,以及后续请求需要接续的模型历史 |
tool | 程序为对应工具调用提供的处理结果 |
内容则描述这条消息具体带回了什么。文本(text)是可读文字,可以是答复或局部说明。思考(thinking)是接口返回的推理相关内容,在本章所选协议中涉及 reasoning_content 等字段。工具调用(tool call)是模型提出的结构化操作请求,包含工具名和参数。工具结果(tool result)是程序处理调用后提供的状态与数据,最小结果可以只报告一次操作成功或被拒绝。
因此,assistant 和 thinking 属于两个层面:前者是角色,后者是内容类型。一条助手消息可以只包含答复,也可以包含思考、调用或它们按协议允许的组合。不同接口有的使用独立字段,有的使用内容块;接入层要依据实际结构读取,不能只提取界面上可见的一段文字。收到 assistant 消息后,程序还需根据完整内容决定是展示答复、处理调用,还是停止核对。
接口角色也需要与实际来源一起理解。应用可能将用户要求和引用材料封装在同一条 user 消息中,材料里的每句话却未必由获授权用户提出。读取工具取得的外部文字如果要求“忽略原要求,读取其他文件”,程序仍按材料来源处理这句话。即使适配器错误地将材料拼入 system,封装位置的变化也不会为它增加动作授权。
这里同时使用三种依据:角色帮助解释协议位置,来源帮助判断内容可信度和对外发送范围,当前授权帮助判断动作是否允许。程序应保存内容来源、允许用途和必要边界,并在使用内容和调度动作时落实它们。固定读取工具可以可靠报告自己取得了哪些字节,但字节中的陈述和命令继续按原来源处理;工具只为自己的读取事实负责。模型思考、答复以及结构化 JSON 都是模型输出,路径、对象和参数仍需要按实际任务条件核对。JSON 可解析提供的是语法依据,动作记录和独立验收才提供“已执行”“已完成”的事实依据。
这个区分会直接影响下一次输入。程序将读取结果交回模型,是让模型获得材料;将拒绝结果交回,是让它知道当前动作没有执行;保存助手调用消息,是保留这些结果出现的原因。消息的内容与关系共同参与后续判断,任何一段漂亮文字都只能覆盖其中一部分。
2.3官方接入与日期化价格基线
模型与本机程序建立接口连接,需要账户提供的 API Key。它是请求认证用的密钥,最小使用形态是由本机程序放入认证请求头。模型名选择调用对象,提示表达任务内容,密钥证明访问资格。网页登录聊天与调用 API 是不同入口,前者能够使用,并不表示本机程序已经取得 API 凭证。
本书采用 DeepSeek 官方接入,保留以下入口供读者自行准备账户与凭证:
- 开放平台:<https://platform.deepseek.com/>,用于注册或登录、查看余额,并按实际需要充值。
- 密钥入口:<https://platform.deepseek.com/api_keys>,用于登录后取得供自己程序使用的 API Key。
- 首次调用文档:<https://api-docs.deepseek.com/zh-cn/>,用于核对请求方式和基础字段。
- 报价页面:<https://api-docs.deepseek.com/zh-cn/quick_start/pricing>,用于核对运行时适用的价格与规则。
准备顺序是进入官方平台,了解报价和账户余额,按需要充值,再获取自己的密钥。已有公开入口核验只到登录页;登录后的按钮、最低充值额和支付选项,由读者实际看到的官方页面确定。随书代码没有公共密钥,也不依赖优惠或第三方中转。自己的凭证由自己的程序使用,无须提供给作者、模型或远程调试人员。
凭证用于程序侧认证,模型消息只承载完成任务所需的信息。密钥应避开源码、前端页面、版本库、截图和日志,也不进入模型可选择的工具环境。后面的启动方式用终端隐藏输入将密钥交给本次请求进程;环境变量的具体继承范围,会在使用该命令时说明。
模型名称与版本记录
本章使用 Chat Completions,基础地址为 https://api.deepseek.com,请求路径为 /chat/completions,调用名为 deepseek-flash。本书沿用的官方信息核验基线日期是 2026 年 11 月 1 日:该日调用名对应 DeepSeek-V4.1-Flash-1017。这一对应关系仅作为该日期的版本基线保留,运行时仍需核对实际版本。
调用名和背后的具体版本分别保存,有助于解释后续差异。稳定调用名以后可能指向另一个版本,实践记录应包含日期、调用名、接口配置,以及响应或官方说明实际提供的版本信息。没有取得的内部版本信息保留为未取得,而不是依据名称自行补齐。
按同一价表比较实践用量
Token 是模型处理文本时使用的计量单元,字符与 token 之间没有固定的一一对应关系。输入还包括消息结构、历史和工具描述;输出费用也不能仅从可见答复的字数推算。一次请求的资源记录应以实际返回的用量字段为依据。
全书实践沿用下表作为价格比较基线。核验日期为 2026 年 11 月 1 日,币种是人民币,单位是每百万 tokens,适用调用名为 deepseek-flash。
| 计费项 | 空闲时段 | 高峰时段 |
|---|---|---|
| 输入,缓存命中 | ¥0.02 | ¥0.04 |
| 输入,缓存未命中 | ¥1.00 | ¥2.00 |
| 输出 | ¥4.00 | ¥8.00 |
这份基线的时间规则按北京时间计算:周一至周五且不属于中国法定节假日,09:00—12:00 和 14:00—18:00 为高峰;其他时间、周末及中国法定节假日为空闲。价格和适用规则可能变化,实际运行前仍应核对当时的官方页面。保留固定日期,是为了比较代码或运行策略所消耗的资源;账户账单另按运行时价格核对。
金额的计算需要先给定用量。例如,假设全部输入未命中缓存,累计输入 10 万 tokens、输出 2 万 tokens,空闲基线为 0.1 × 1 + 0.02 × 4 = 0.18 元,高峰基线为 0.36 元。累计输入 100 万、输出 20 万时,两种基线金额分别为 1.80 元和 3.60 元。
这些是假设用量的算例,随书记录尚未提供真实消费。历史增长、输出长度和重跑次数都会影响累计值,另有收费的工具与服务也需要单独计入。本书不承诺免费运行,也不承诺几元可以跑完全书。读者首先约束请求规模,并逐次保存实际用量,再进行成本比较和账单核对。
2.4首次请求的输入与成功条件
第一份程序只处理纯文本接入:请模型用一句话说明消息与模型请求的区别。这个小问题使要求和答复容易辨认,工程检查则集中在认证、载荷、传输、返回解释与失败退出。工具执行和任务验收留待后续循环。
程序使用 Python 3.11 及以上版本的标准库,无需安装 SDK。载荷选择 deepseek-flash,设置 stream=False,并显式写入 thinking.type=disabled。按本书沿用的接口基线,thinking 默认启用;明确关闭后,首次接入可以一次取得非流式完整返回,专注于文本消息。以后采用流式或启用 thinking,需要增加相应字段与接续处理。
请求内有两条消息:system 给出简体中文、简短回答的基础要求,user 提出本次问题。它们连同模型和输出配置一起编码为 UTF-8 JSON 字节;认证密钥另外进入请求头。发送前检查字节规模,取得响应后检查读取规模,再解释 JSON 中的 choices 和助手消息。这条流程把“准备输入”与“解释返回”落实为明确步骤。
首次请求的成功条件是:finish_reason=stop,消息角色为 assistant,没有工具调用,content 是去除首尾空白后仍非空的字符串。程序取得的文本还要满足完整结束条件;finish_reason=length 表示生成在长度限制处终止,不能据此当作完整答复输出。空白文本和错误内容类型也会进入失败退出。
这里的成功属于纯文本接入范围。程序没有提供工具,因此工具调用返回属于意外情况;工具循环会另外识别调用分支。HTTP 或网络失败表示本机没有取得可用结果,JSON 或消息结构失败表示返回无法按当前约定解释,这些状态也分别保留。
源码在检查答复成功以前先打印解析到的 usage。因此,长度结束等失败仍可能留下已取得的用量;usage 未取得时打印 null,含义是未知而非零消耗。书中不预填应当出现的模型句子或 token 数,真实返回由实际调用取得。
完整源码:book/v2/first_request.py
以下是随书首请求程序的完整源码。main() 中的消息载荷、发送与读取、成功条件组成主流程;NoRedirect 和 scrub 分别处理请求跳转及诊断脱敏。传输与诊断的具体边界在下一节集中解释,以便将接入原理和实现细节分别阅读。
从本书根目录运行的入口是 python3 book/v2/first_request.py。源码也可另存为 first_request.py 独立使用;隐藏输入命令仍以随书目录为运行位置。程序只发出一次请求,运行前应准备自己的凭证和账户余额。
import json
import os
import re
import sys
import urllib.error
import urllib.request
ENDPOINT = "https://api.deepseek.com/chat/completions"
MAX_PAYLOAD_BYTES, MAX_RESPONSE_BYTES = 32_768, 262_144
MAX_ERROR_BYTES = 4_096
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def scrub(value, key, clipped=False):
text = str(value)
if clipped and key:
for size in range(min(len(key), len(text)), 0, -1):
if text.endswith(key[:size]):
text = text[:-size] + "[已脱敏片段]"
break
if key:
text = text.replace(key, "[已脱敏]")
text = re.sub(r"(?i)\bBearer\s+[^\s\"'<>,;]+", "Bearer [已脱敏]", text)
return re.sub(r"(?i)\bsk-[A-Za-z0-9_-]*", "[已脱敏]", text)
def main():
key = os.environ.get("DEEPSEEK_API_KEY", "")
if not key or any(ord(ch) < 33 or ord(ch) > 126 for ch in key):
print("缺少或无效的 DEEPSEEK_API_KEY。", file=sys.stderr)
return 1
payload = json.dumps({
"model": "deepseek-flash", "thinking": {"type": "disabled"},
"messages": [
{"role": "system", "content": "请用简体中文简短回答。"},
{"role": "user", "content": "用一句话说明消息与模型请求的区别。"}
],
"max_tokens": 128, "stream": False
}, ensure_ascii=False).encode("utf-8")
if len(payload) > MAX_PAYLOAD_BYTES:
print("请求载荷超出字节上限。", file=sys.stderr)
return 1
request = urllib.request.Request(ENDPOINT, data=payload, headers={
"Authorization": "Bearer " + key, "Content-Type": "application/json"
}, method="POST")
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
print("准备发出一次模型请求;不自动重试。", file=sys.stderr, flush=True)
try:
with opener.open(request, timeout=30) as response:
body = response.read(MAX_RESPONSE_BYTES + 1)
except urllib.error.HTTPError as exc:
try:
with exc:
raw = exc.read(MAX_ERROR_BYTES + 1)
clipped = len(raw) > MAX_ERROR_BYTES
detail = scrub(raw[:MAX_ERROR_BYTES].decode("utf-8", "replace"), key, clipped)
note = f"仅保留前 {MAX_ERROR_BYTES} 字节,已截取" if clipped else "未截取"
except (urllib.error.URLError, TimeoutError, OSError) as read_exc:
detail = scrub(getattr(read_exc, "reason", read_exc), key)
note = f"错误 body 读取失败({type(read_exc).__name__})"
print(f"HTTP {exc.code};{note};body={json.dumps(detail, ensure_ascii=False)};"
"不自动重试。", file=sys.stderr)
return 1
except (urllib.error.URLError, TimeoutError, OSError) as exc:
detail = scrub(getattr(exc, "reason", exc), key)
print(f"网络错误({type(exc).__name__}):{json.dumps(detail, ensure_ascii=False)};"
"不自动重试。", file=sys.stderr)
return 1
if len(body) > MAX_RESPONSE_BYTES:
print("响应超出字节上限。", file=sys.stderr)
return 1
try:
data = json.loads(body)
print(scrub(json.dumps({"usage": data.get("usage")}, ensure_ascii=False), key))
choice = data["choices"][0]
message = choice["message"]
content = message["content"]
if (choice["finish_reason"] != "stop" or message["role"] != "assistant"
or message.get("tool_calls") or not isinstance(content, str)
or not content.strip()):
raise ValueError("不是完整的非空文本答复")
except (ValueError, KeyError, IndexError, TypeError, AttributeError) as exc:
print(f"响应错误({type(exc).__name__}):JSON 或消息结构不符、"
"未完整结束或答复为空;不输出原始响应。", file=sys.stderr)
return 1
print(scrub(content.strip(), key))
return 0
if __name__ == "__main__":
sys.exit(main())
启动时的凭证传递与环境边界
以下命令在交互终端隐藏输入密钥,外层 Python 随后将它放入本次请求子进程的环境。从本书根目录执行:
python3 -c '
import getpass, os, subprocess, sys
if not sys.stdin.isatty():
raise SystemExit("请在交互终端运行。")
env = os.environ.copy()
env["DEEPSEEK_API_KEY"] = getpass.getpass("DeepSeek API Key: ")
raise SystemExit(subprocess.call([sys.executable, "book/v2/first_request.py"], env=env))
'
密钥没有写入明文 export 命令,也没有作为命令行参数传递,因而避免直接进入 shell 历史和进程参数。外层程序不修改父 shell 的环境,子进程退出后不留下长期 shell 配置。输入过程也不应渲染到界面或纳入运行记录。
这一方式缩小了传递范围,环境变量仍可被进程后代继承,也可能被同一运行环境中具有足够权限的组件接触。这里复制的环境只交给发起请求的程序。第3章接入模型可选择的工具时,应为工具另行构造环境,剔除 DEEPSEEK_API_KEY 等模型访问凭证,避免将请求程序的认证能力随工具一起传下去。隐藏输入和子进程传递各自承担有限的保护作用,不能替代进程隔离。
2.5请求程序的传输边界与诊断
首次接入设置了几种不同的数量边界。编码后的载荷最多 32,768 字节;max_tokens=128 约束生成长度;正常响应最多读取 262,145 字节,其中最后一个字节用于判断是否超出 262,144 字节的接受上限。响应 body 还包含 JSON 结构及其他字段,因此读取字节界限独立于输出 token 上限。密钥非空且字符码均在 33—126 范围内时才建立传输,这项检查仅说明本地格式可接受,凭证是否有效仍由官方服务认证。
程序将请求固定发往官方 HTTPS 地址。ProxyHandler({}) 排除环境代理,NoRedirect 禁止自动重定向,避免认证头跟随跳转到其他主机。发送前的 stderr 提示表示准备发起请求;是否取得可用返回,要看后续处理。timeout=30 设置的是网络操作超时,不能据此承诺整个任务必在 30 秒内结束。
失败诊断保留定位问题需要的信息,同时限制读取与输出。HTTP 错误记录状态码,错误 body 最多读取 4,097 字节,只保留前 4,096 字节,并标明是否截取。错误 body 读取失败时,仍记录 HTTP 状态、读取异常类型和脱敏原因。网络错误记录异常类型与脱敏后的 reason;JSON 或消息结构错误仅记录异常类型和固定原因,不输出原始响应。
scrub 遮蔽当前密钥、Bearer 凭证和 sk 模式。被截取的错误 body 如果以当前密钥的一个前缀结束,也会遮蔽该片段。请求对象和请求头不会打印,诊断中的控制字符通过 JSON 编码转义显示。正常输出的 usage 和文本也经过同一凭证脱敏,处理服务意外反射凭证的情况。这是针对已知密钥与模式的防护,其他机密仍可能无法自动识别,诊断只应在本机终端使用。
程序遇到 HTTP 错误、网络错误、超时或过大响应后退出,不自动重试。请求可能已经到达服务,只是本机没有取得结果;重复发送可能增加费用,退出也不能证明服务未处理或未计费。成功退出码是 0,所处理的失败退出码是 1。退出码表达本地处理状态,用量是否取得、费用是否确定需要另外记录。
首请求已经检查的范围
这份实现读取 choices 的第一项,并检查其中的结束状态、助手角色、工具调用真值和非空文本。它没有检查 choices 恰好只有一项,也没有检查返回的非空 reasoning_content 是否与关闭 thinking 的配置冲突。tool_calls 采用真值判断,没有严格区分所有空值形态的类型。usage 被打印,但其字段结构、计量口径和任务累计费用尚未校验。
这些范围决定了如何使用结果:首程序可以验证限定条件下的文本接入,不能充当完整工具响应验证器或预算记账器。第3章的工具循环还需要在调度前核对完整响应、模式匹配及调用关系,并建立逐次用量记录。阅读源码时,应以实际检查为准,不将后面描述的循环要求提前算作首请求已经实现的能力。
2.6用量记录、成本比较与运行预算
一次响应中的 usage 描述本次模型请求。任务经历多次往返时,程序要先将请求编号、时间、模型和配置、实际 tokens、计价时段(tariff)及本次适用价格关联到同一任务,再计算累计值。Tariff 在这里指请求适用的计价时段和规则,最小记录可以标明该请求采用空闲还是高峰价格。
连续请求会重复使用历史。第一次提交用户要求,第二次提交同一要求与读取结果,重复的要求仍属于第二次输入;工具描述也会进入输入用量。几次可见答复都很短,累计输入却可能随着历史增长。这是把用量按请求记录、再按任务汇总的直接原因。
按本书沿用的官方 Chat Completions 契约,prompt_tokens 是输入 token 数,等于 prompt_cache_hit_tokens 与 prompt_cache_miss_tokens 之和;completion_tokens 是生成的输出 token 数;total_tokens 是输入与输出之和。后续记账程序需要核对这些字段及其关系,首请求打印的对象只是所取得的原始用量信息。
用量记录还应表达信息是否齐全。缓存细分未取得、但输入和输出总量可用时,可以明确按全部输入未命中进行保守估算,并保留“缓存细分缺失”的标记。如果输入、输出总量也未取得,费用就是未知。字段缺失、显式 null 与数值 0 含义不同;前两者不能补成零,失败请求尤其需要保留这种未知状态。
口径成立后,本次费用由三项相加:命中输入 tokens 乘命中单价,未命中输入 tokens 乘未命中单价,输出 tokens 乘输出单价,各项除以一百万。任务费用逐次求和。跨时段运行时,每次请求保留自己的适用价格,不能用最后一次单价覆盖全部历史。
资源比较与账户核对分别计价
基线成本使用实际用量和本章固定日期价表,帮助比较代码版本或运行策略;最小比较可以将两次运行都按同一空闲基线计算,也可以分别报告空闲、高峰两套金额。账单估算使用每次请求发生时实际适用的价格与规则,帮助核对账户花费。官方调价以后,同一份用量得到不同金额是可能的,因此记录要同时保留价表日期、时段和两套价格来源。
只保存一个“总成本”,以后难以辨认变化来自资源消耗还是单价。本地估算也不替代官方平台的最终结算;真实调用之后,仍需核对账户消费。
缓存命中降低的是部分输入价格,命中的历史仍进入模型输入并占用上下文窗口。它不会替程序选择消息,也不能据此认为会话已被服务永久保存。优化价格和组织上下文需要各自处理。
预算在下一次请求之前落实
运行预算是程序允许这次运行使用的资源额度。最小形式可以是只允许一次模型请求;进入循环后,还需要限制输入规模、单次输出和工具资源。本章首程序的三项边界分别是载荷字节上限、max_tokens 和固定一次请求,它们约束不同的量。
载荷字节检查发生在发送之前,超过界限就停止或重新组织输入。字节界限便于在本机实施,精确 token 预算则还需核对所选模型的计量与窗口契约。max_tokens 约束本次 completion,最大模型请求数约束计算次数,工具往返后的再次请求也计入。较小输入可以被反复提交,较短输出也可能伴随长历史,因此三种限制应同时建立。
循环在发出下一次请求前检查剩余额度,耗尽时进入明确停止状态。模型请求次数不限制工具自身的收费、运行时间或其他资源,这些由工具设置独立边界。第3章会将逐次记录与请求次数限制纳入固定任务循环。
启用 thinking 时,completion_tokens_details.reasoning_tokens 可以提供思考输出细分。费用仍按总输出 completion_tokens 计算,max_tokens 限制的 completion 也包括思考输出。只记录可见答复会遗漏这部分,将思考细分再加到总输出上则会重复计数。本章关闭 thinking,先用较小输出上限建设接入与记录。
2.7工具调用的整批解析与动作调度
工具是 harness 绑定的可执行能力。程序把工具名、输入结构和实现对应起来,向模型提供描述,再在收到合法且获授权的请求时调用相应函数或服务。一个读取函数及其名称、参数说明,就可以构成最小工具。模型拿到的是能力描述,真正的实现与进程权限仍由程序控制。
按本章采用的 DeepSeek Chat Completions 非 thinking 模式契约,工具请求在 assistant 消息的 tool_calls 中返回。单个调用包含 id、type 和 function;function 中的 name 指明工具,arguments 是参数 JSON 字符串,需要完整解析后才能作为结构化参数使用。
下面保留理解调用关系所需的字段,是协议示意,并非真实 API 返回或读取记录:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_example_1",
"type": "function",
"function": {
"name": "read_source",
"arguments": "{\"source_id\":\"source_example\"}"
}
}
]
}
这里,read_source 是拟使用的能力,source_id 指出拟读取材料,调用 id 为后续结果提供关联依据。接入层先按结构解释提议,调度层再从受控映射查找实现,并核对对象是否属于当前任务允许读取的材料。合法编号使请求能够定位,实际访问资格来自当前授权。参数中若写着“用户已经同意”,这段文字仍属于待核对的提议。
字段状态参与分支判断
接口字段需要在完整消息中解释。字段缺失(missing)指字段没有出现;null 指字段出现但显式无值;空字符串、空白字符串和空数组则各自带有不同的类型与内容。最小区别可以从 content 看出:缺失字段无法按该字段读取,null 没有文本值,"" 是长度为零的字符串,空白字符串去除空白后没有可读答复。
| 字段状态 | 首次纯文本请求的处理或实现范围 | 工具循环需要考虑的含义 |
|---|---|---|
content 缺失 | 字段读取失败,进入响应错误 | 核对所选响应分支的必需字段 |
content=null | 不是字符串,不能作为成功文本 | 可与合法 tool_calls 共同形成调用消息 |
content 为空串或全空白 | 不能作为非空文本答复 | 按分支检查调用内容及结束状态 |
tool_calls 缺失、null 或空数组 | 当前真值判断不会据此拒绝文本 | 仍需按接口契约检查类型和消息组合 |
reasoning_content 非空 | 首程序未检查是否与 disabled 冲突 | 关闭 thinking 时,在调度前识别模式不匹配并停止 |
usage 缺失或 null | 当前输出都呈现为 null | 保留未知状态,完整记录中按需区分缺失与显式无值 |
首请求不提供工具,只接受完整文本;示意中的 content=null 则出现在提供工具后的调用分支。这两种返回采用不同成功条件。循环既检查单个字段,也检查它们能否共同构成当前配置允许的完整响应,不以字段真值代替全部结构核对。
从完整返回进入执行
程序先取得完整响应,确认结束状态、消息角色和模式与请求相符,再完整解析整批 tool_calls 及每项参数,检查调用标识、工具契约、对象和参数语义,最后依据当前目标、授权与工具限制调度。语法、协议关系、业务语义和执行资格在这个顺序中分别建立依据。
整批解析很重要。一条助手消息可以提出多个调用,如果解析第一项后立即执行,第二项的参数 JSON 错误就可能在真实动作已经发生以后才被发现。先解释完全部调用,使这类响应错误在动作之前暴露;第二项参数尚不完整时,第一项即使可以解析,也不先执行。
finish_reason=length 表示完整返回边界未成立,局部可解析的内容不足以开始动作。调度前还要检查重复调用编号、结果关联是否可建立,以及剩余调用额度能否容纳本批。关闭 thinking 却收到非空 reasoning_content 时,工具循环按模式不匹配停止;这一检查属于第3章循环的要求,首程序尚未实现。
整批解析提供的是执行前的协议检查,业务事务需要另行设计。真正开始执行后,某项可能成功,后项可能被拒绝或失败,已发生的效果不会因前面的解析自动回滚。参数问题能够按工具契约表达时,可以形成明确的错误结果;工具未知、标识无法关联或响应无法确认时,则停止核对,避免制造执行路径或结果关系。
工具名称通过受控映射查找,不由字符串推测可执行路径。以后使用流式接口,参数分片也须等待完整边界成立后再执行。本章采用非流式,是为了先建立这条清楚的顺序。执行后的事实则由工具记录与后续核对提供:读取记录关联取得的材料版本,创建记录关联真实新对象,最终判断据此形成。
2.8工具结果的关联与后续请求
工具执行或拒绝调用之后,模型需要在下一次输入中取得结果。程序先保存本次返回的助手调用消息,再处理其中各项调用,随后为每项追加匹配的 tool 消息,最后携带这些历史发出下一次模型请求。助手消息在前,工具结果在后,原请求和处理结果一起构成后续判断的依据。
tool_call_id 将结果关联到原调用的 id。延续上一节示意,假设程序确认拟读取材料不在授权范围,可以形成以下结果:
{
"role": "tool",
"tool_call_id": "call_example_1",
"content": "{\"status\":\"denied\",\"reason\":\"source_not_authorized\"}"
}
这是一份协议示意,并非已发生的拒绝日志。它表达程序处理了该调用而没有执行读取。模型取得拒绝状态后,可以报告当前限制,或在允许范围内重新选择。能够按工具契约表达的参数错误,也以带原编号的结果回传;这样,每项可合法关联的调用都有明确去向。
调用标识或消息结构无法合法关联时,程序停止核对,不自造编号补关系。只保留工具结果而丢掉助手调用消息,也会失去结果对应的原请求。多调用情况下,应处理完所有有效调用并形成相应结果,再让模型继续,不能执行第一项后就略过同批其他项。
调用数组允许模型提出多个请求,具体调度仍由 harness 决定。本书初版串行处理。工具之间是否存在依赖、一个错误之后哪些动作还能发生、是否允许并发,需要结合工具的执行条件与效果设计;数组本身不会提供这些保证。
工具往返是助手调用及其匹配结果进入后续模型输入的过程。最小形态是一条调用消息、一条带对应标识的结果消息和随后的一次请求。它在原用户 turn 内推进工作,结果回来以后,模型可能继续选择工具,也可能报告限制或形成文本答复。再次请求以前,程序仍需检查预算和当前有效条件。
本章沿用的当前 Chat Completions 接口不支持在既有历史中间插入自造工具调用。程序不能为缩短消息,伪造模型没有发出的调用去配对旧结果。删除原调用、交换关联标识或拆散未完成关系,都可能改变合法接续所依赖的信息。历史分支与压缩应按接口契约建设,不能将 messages 当作可任意拼接的文字列表。
固定摘录任务中的推进关系
在这个固定任务中,读取结果把获授权材料交给模型,模型据此提出创建参数;创建结果把实际产物及状态交回,模型再说明处理结果。这个局部例子使两次信息变化容易看清:第一次增加的是材料,第二次增加的是执行事实。模型每次选择都依赖程序实际提交的内容。
第3章将完整搭建 read_source 与 create_card 两个固定工具及其运行循环。目标是从指定材料中使用真实摘录创建新产物,而非开放式地让模型自由选择文件或服务。那里会继续建设目标与授权状态、动作前记录、预算与停止路径,以及独立于模型答复的产物验收。本章提供的是接入和消息关系基础,尚未实现这套执行器。
下一次模型请求被服务接受,只说明输入得到处理并产生响应。工具是否执行、产物是否存在、是否满足任务要求,分别由执行记录和独立验收核对。将这些事实与原调用关联起来,最终答复才有可追溯的依据。
2.9thinking模式下的历史接续
thinking 增加的不只是输出内容,还包括费用细分与历史接续条件。本章首请求关闭它,以便先读清文本和工具关系;启用时,接入实现需要依据所选接口保存合法历史。DeepSeek 官方思考模式文档为 <https://api-docs.deepseek.com/zh-cn/guides/thinking_mode>,以下沿用本书日期化核验基线中的特定要求。
在该契约下,启用 thinking 且请求携带 tools 时,后续接续必须完整回传历史 reasoning_content。触发条件是请求携带 tools,而非模型实际上是否发出过工具调用。即使本次只返回文本,这项历史要求仍然存在;新用户 turn 开始也不会自动解除它,相关跨用户 turn 历史同样需要按契约保留。不带 tools 时,该项可以忽略,接入层据此采用相应分支。
因此,合法助手历史要保存协议要求的字段,而不只保存可见 content。思考内容在语义上是模型输出,用于参与后续判断;在协议上可能是必需的接续项。这两个用途并行存在:完整保存它维护接口关系,事实可信度和动作授权仍按前述来源与执行规则判断。
本章直接用标准库构造载荷。如果改用 OpenAI SDK 接入 DeepSeek,扩展参数 thinking 需要通过 extra_body 显式传入。SDK 提供传输封装,模式选择、字段核对与历史接续仍需在应用接入层落实。换接口时也要重新核对字段与契约,而不只更换基础地址。
长任务需要减少输入时,应先分辨业务材料与接口必需的接续项,再选择允许的历史分支或压缩方式。业务摘要可以帮助重新组织材料,但普通摘要不能替代完整 reasoning_content;按用户 turn 一律清空思考历史也不能作为通用规则。第7—9章会进一步讨论上下文组织与管理,本章先确立合法接续所依赖的边界。
验证记录:本地模拟与真实服务的范围
随书 book/v2/test_first_request.py 通过模拟传输检查首请求程序。已有记录如下:
| 记录项 | 内容 |
|---|---|
| 日期 | 2026 年 11 月 1 日 |
| 对象 | book/v2/first_request.py 的本地处理逻辑 |
| 方法 | 模拟传输,凭证与用量均为合成值 |
| 结果 | 12 项通过 |
| 真实服务 | 真实 DeepSeek API 尚未联调,无实测消费记录 |
检查覆盖正常文本、凭证反射脱敏、长度结束、空白答复、意外工具调用、HTTP 与网络错误,以及代理、重定向、凭证和字节边界。畸形响应包括 JSON 解码失败、错误顶层类型、空 choices、错误 choice 类型、字段缺失与 content=null。测试环境和完整系统验证记录列在第3章。
这些结果说明程序如何处理构造出来的返回,不能证明真实服务兼容性,也不能登记为账户消费。没有 API Key 时,可以据此理解并建设载荷组装、传输替身和结果解析;准备真实凭证后,才有条件通过实际请求核对接口返回、用量与账户消费。目前的记录仍停留在前一层。
本地验证检查给定返回下的程序行为,真实联调检查服务实际提供的字段、状态和兼容性,账户核对检查实际消费。三层分别留下记录,才便于定位问题;本地用量估算最终仍需与官方平台结算核对。
本章判据
本章的完成条件覆盖概念理解、实际接入和协议接续。读者应分别核对已理解的结构与已经取得的运行证据;只有模拟结果时,不将真实接入项勾选为完成。
- ☐能定义会话、用户 turn、消息和模型请求,指出任务与会话的不同范围,并将工具后的再次请求关联到原用户 turn。
- ☐能区分接口角色、内容类型与实际来源,解释模型提议、工具报告和材料内容的用途,并在使用处核对可信度、对外发送范围与动作授权。
- ☐实际接入时从官方平台取得自己的凭证,核对余额和运行时价格,确保密钥不进入源码、消息、工具环境或日志。
- ☐首次请求明确指定模型和 thinking 配置,设置载荷、输出与响应读取边界,关闭自动重定向,不自动重试。
- ☐能根据真实返回核对完整结束状态和
usage;只有模拟数据或尚无凭证时,明确记录真实 API 未联调。 - ☐能区分字段 missing、
null与各类空值,知道首请求对 choice 数量、非空reasoning_content、tool_calls空值类型和usage结构仍有哪些未检查范围。 - ☐逐次保存实际用量、tariff 和适用价格,分别计算日期化基线成本与账单估算;用量缺失保留未知,不补零。
- ☐能说明完整批次解析、参数校验与执行授权的顺序,截断响应不开始动作,整批解析也不提供业务事务回滚。
- ☐先保存助手调用消息,处理各项调用,再追加带原
tool_call_id的匹配结果,随后请求模型继续;拒绝和可表达的参数错误同样回传。 - ☐知道 thinking 与
tools组合下完整回传历史reasoning_content的特定要求,包括未实际调用工具和跨用户 turn 的情况,普通摘要不能替代该历史。 - ☐能区分协议示意、已有本地验证和真实服务联调,知道产物存在及任务验收需要独立于模型声明核对。
本章没有回答什么
本章没有提供完整工具执行器,也尚未实现副作用前记录、目标与授权变更的处理、未知结果处理和产物验收。模型 API、供应商产品及各种上下文压缩方法未在这里全面展开。首次取得文本或 JSON,即使请求和用量记录成立,也只完成了部分接入工作。
第3章围绕固定摘录任务完整搭建:固定工具读取获授权材料,依据真实摘录创建新产物,运行系统在有状态、有预算、可停止的循环中组织过程,再由独立验收确认结果。消息关系保留模型提出的请求与程序回传的结果,执行记录保留实际动作,验收确认最终产物。这几层结合以后,任务才形成完整且可以核对的运行过程。