大模型通识 / 大模型应用开发入门
10. 工具调用:把确认过的安排交给程序
原文备份 · 第 12 / 19 篇 · 查看极客时间原知识库 ↗
本篇目录 · 10 节
来源:大模型应用开发入门
笔记助手已经会制作复习卡。如果你说“把N1和N2安排在10月5日复习20分钟”,它也可能写出一段很像计划的文字。但屏幕上出现“已经安排好了”,不代表任何文件真的保存了,更不代表日历里有了提醒。
本篇让这句话经过一条可核对的执行链:模型提出结构化安排,程序校验,用户确认,本地函数保存JSON,再把结果交还模型解释。目标是完成一次真实的本地操作,并清楚地区分“提出请求”“允许执行”“已经执行”这三件事。
承接第09篇的独立快照,使用 Python 3.11+、openai==3.24.0 和 DeepSeek 官方 API。接口核实日期为2026年10月4日。必做演示不联网、不读Key;输入确认词后会实际写入演示目录。真实模式可能产生最多两次模型调用费用,本课没有代你做线上调用。
一、工具调用到底是谁在执行
模型并不直接运行你的Python函数。应用先把可用工具的名称和参数说明发给它,它返回“建议调用哪个工具、传哪些参数”,应用再决定是否执行。官方指南也明确说明,具体函数由开发者提供。DeepSeek:Tool Calls
这次我们只提供一个工具:save_review_plan,中文含义是“保存复习计划”。它只能在当前项目的固定目录中新建一个JSON文件,不操作日历、不发消息,也不执行命令。
用户提出安排
↓
模型返回工具名称 + arguments字符串
↓
程序检查工具和参数
↓
展示规范化后的计划,等待用户输入SAVE
↓
本地函数保存,或记录用户取消
↓
把真实结果作为tool消息回传
↓
模型解释结果,流程结束
这张图中,模型给出参数之后、Python写文件之前,还有两道门:程序校验与人的确认。工具描述里写“请先确认”可以帮助模型理解,但不能代替这两道实际程序逻辑。
二、先把工具做小
我们沿用项目里的N1、N2、N3三条笔记。计划只需要四个字段:
| 字段 | 例子 | 本课约束 |
|---|---|---|
| title | RAG基础复习 | 去除两端空白后1—80字符,不含不可打印字符 |
| note_ids | ["N1", "N2"] | 1—3个现有笔记ID,不能重复 |
| date | 2026-10-05 | 有效日历日期,严格YYYY-MM-DD形式 |
| minutes | 20 | 1—120的整数,不能是字符串或布尔值 |
日期只表示复习日,分钟数只表示持续时长。本课没有时区、具体开始时间、通知渠道和日历接入,因此保存成功也不会自动提醒你。这个范围足够小,你可以直接打开文件检查。
参数中没有 path、command、code 或 approved。保存位置由应用生成,是否确认由终端输入决定,模型不能靠加一个 "approved": true 越过确认。
review_tool.py 中的 TOOLS 定义了名称、说明和参数结构。required 说明四项都需要,additionalProperties: false 表示不应多带字段。但这些描述不能代替本地检查,应用仍会解析并验证实际返回的数据。
这是一个适合入门的权限设计:给模型一个用途明确的小动作,而不是让它决定任意文件路径,再寄希望于提示词能覆盖所有风险。
三、这与复习卡的JSON输出有什么区别
复习卡入口要求模型的正文 content 是一个JSON对象。工具调用则把函数名和参数放在 message.tool_calls 中,其中 function.arguments 是一段JSON字符串;应用需要再解析它。
一个作者构造的提议大致如下,省略外围响应字段:
{
"id": "demo-call-01",
"type": "function",
"function": {
"name": "save_review_plan",
"arguments": "{\"title\":\"RAG基础复习\",\"note_ids\":[\"N1\",\"N2\"],\"date\":\"2026-10-05\",\"minutes\":20}"
}
}
不要把arguments当Python代码执行,也不要用 eval()。我们复用第06篇的JSON解析函数:拒绝重复键、非标准数值和过长内容,再检查字段、类型、日期和笔记ID。函数名必须等于白名单中的 save_review_plan,没有动态导入或按任意函数名执行的通道。
本课新建 planner.py 入口,保留 cards.py 原来的结构化复习卡任务。工具请求使用非流式、关闭思考,第一步为 tool_choice: auto,第二步为 none。auto 允许模型选择提议工具或直接询问缺少的信息;none 用于工具执行后的解释阶段。DeepSeek:tools与tool_choice参数
这里不传复习卡的 response_format: json_object,因为最终说明可以是普通文字。也没有把卡片的 --thinking、--attempts 参数扩展到这个新入口。先把一次完整协议流程讲清,再增加模式和循环,比同时混合所有功能更容易验证。
官方另有strict模式,但当前它属于Beta并要求相应地址和配置。本课用普通接口与本地验证,不把普通工具声明写成服务端严格保证。即使以后使用strict,合理日期、用户意图和是否允许写入,仍需要应用检查。DeepSeek:strict模式说明
四、准备独立代码,先看将要发什么
下载 lesson-10完整代码包,解压并进入 lesson-10 目录。新增三个文件:
review_tool.py:工具说明、参数检查与本地保存。tool_flow.py:模型请求、确认、执行、结果回传和停止。planner.py:命令行入口与固定离线演示。
macOS / Linux:
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
cp config.example.json config.json
.venv/bin/python planner.py --dry-run
Windows PowerShell:
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
Copy-Item config.example.json config.json
.\.venv\Scripts\python.exe planner.py --dry-run
已有配置不要覆盖。Windows只有 python 时,可替换 py -3。依赖与此前相同,安装需要网络,预演不会联网或写记录。
预演会打印首次请求:模型来自配置,两条消息包含说明和笔记,tools 数组只有一个函数,tool_choice 为 auto,thinking.type 为 disabled,stream 为 false。检查它没有额外工具、任意路径或授权字段。
默认输入是“请把N1和N2安排在2026-10-05复习20分钟,标题为RAG基础复习。只保存本地计划”。它是固定教学日期,不会自动理解成你运行当天的“明天”。实际使用请给出自己要的日期。
五、离线走完一次确认保存
macOS / Linux:
.venv/bin/python planner.py --demo
Windows PowerShell:
.\.venv\Scripts\python.exe planner.py --demo
演示中的模型回复由作者构造;它不会真正请求模型。程序会打印本次记录目录、规范化的四个字段和目标文件,最后提示:
仅输入大写SAVE才保存;其他输入取消:
先核对日期、笔记和分钟数,再输入大写 SAVE 并回车。不要在命令行里预先绕过这一步;正是这一步把模型提议变成你的明确决定。
成功后应该先看到程序自己的“本地文件已保存”,随后才看到模型演示说明。新文件位于 demo-plans/一串ID.json,例如内容结构为:
{
"simulated": true,
"plan": {
"title": "RAG基础复习",
"note_ids": ["N1", "N2"],
"date": "2026-10-05",
"minutes": 20
}
}
ID每次不同,以终端打印的实际路径为准。打开那个文件,独立核对四个字段和 simulated: true。这是判断保存成功的依据,不是最后一段自然语言说了什么。
程序同时在 tool-records/同一串ID/ 下保存五个文件:两次请求、两次响应和 summary.json。summary会在过程中更新,最终保存 status: complete、approved: true、tool_result.status: saved。它不是追加式事件日志。
再运行一次演示,这次直接回车。应看到程序确认取消;这次summary的 approved 为false、工具结果为cancelled,本次打印的目标文件不应存在。以前保存的文件不会被删除,因此不能只看目录是不是空的。
离线演示确实会写演示文件,但不会写到真实模式的 plans 目录,也不产生通知。改动 --request 不会改变固定演示;程序会拒绝把自定义请求与 --demo 混用,防止你误以为演示是在理解新任务。
六、确认前,程序到底检查了什么
看到提议后,程序不会立刻问“保存吗”。它先检查响应只有一个选项,工具调用是完整的 tool_calls 结束状态,并且本课只出现一项工具提议;出现多项、截断、未知函数或错误参数时,直接停止。
例如,模型返回 "minutes": "20",对人来说很好理解,但程序要求整数20。它不会悄悄转换,再让你确认一个与原始提议有所不同的动作。不存在的N9、无效日期2026-02-30、重复笔记ID、多余的文件路径,也会被拒绝。
通过结构检查之后,程序展示清理过两端空白的标题和完整计划,让你核对是否符合自己的原意。日期格式正确不等于日期就是你想要的,字段合法不等于模型理解正确,这一层仍需要人判断。
保存函数使用应用生成的路径,并以独占创建方式打开文件:
with path.open("x", encoding="utf-8") as file:
json.dump({"simulated": simulated, "plan": plan}, file, ensure_ascii=False, indent=2)
x 的意思是只创建新文件,如果目标已存在便报错,不覆盖。它不能保证断电时文件仍完整,也不是跨进程数据库事务;写入出错时要打开目标文件检查,不能自动认定完全没写过。Python:open文件模式
每次新运行会生成新ID。它能避免同一路径被覆盖,却不会自动识别“两次运行其实是同一个复习安排”。不要把随机ID当成业务去重;重复启动并再次确认,仍可能保存两份相同计划。持久化去重将在第14篇继续处理。
七、执行结果怎样交还模型
模型提出工具后,下一次请求不能只写一句“保存好了”。程序需要保留原assistant工具提议,并添加与其ID对应的tool消息。配套代码原样如下:
messages.append({"role": "assistant", "content": message.get("content"), "tool_calls": calls})
messages.append({"role": "tool", "tool_call_id": call["id"],
"content": json.dumps(state["tool_result"], ensure_ascii=False)})
此时消息顺序为 system → user → assistant工具提议 → tool执行结果。tool_call_id 用于说明结果对应哪项提议;它由响应提供,只用于协议关联,不作为磁盘路径。
打开本次 02-request.json,可以独立验证这四条消息、相同的调用ID、真实的saved或cancelled,以及 tool_choice: none。确认取消也会回传cancelled,不能编造一次成功结果来让对话看起来完整。
第二次响应如果还要求工具、正文为空或未正常结束,程序会停止,不再进行第三次调用。这是应用中的硬边界;提示模型“不要循环”只是补充说明。
模型解释仍可能说错,所以界面明确把它标为“模型说明”,把程序保存结果另行打印。如果文件确实保存了,模型却说没有保存,应以本地文件与程序结果核对;反过来,模型说“已加入日历”也不会凭空产生日历事件。
八、最容易误判的失败:文件已保存,总结却失败
考虑这条过程:
第一次模型请求成功 → 用户SAVE → 文件保存成功 → 第二次请求超时
整个流程最后可能显示 stopped,但保存动作并没有因此撤销。summary.tool_result.status 仍应是saved,计划文件也仍在那里。程序不会为了得到一句漂亮的总结,再把整个流程重跑。
本篇新工具入口没有自动重试,客户端仍为 max_retries=0,最多发两次模型请求、最多执行一次本地动作。第08篇的有限重试仍保留在卡片入口;不要把生成文本的重试循环直接包在“请求—写文件—再请求”整段代码外面。
程序在写计划前先保存执行意图。如果这一步失败,便不执行;写计划成功但后续记录失败时,则停止并提示核对实际文件。如果进程被强制终止,记录也可能只停在执行中。只有文件与记录共同提供足够证据,才能判断发生了什么。
等待确认时按Ctrl+C会中断,不保存计划;按回车或输入其他字符表示取消。若在写入阶段中断,不能只凭Ctrl+C就断言没有副作用,应核对打印出的目标路径。
九、真实调用和费用记录要分别看清
完成离线验证后,如果你决定用自己的账户,可运行下面命令。它会隐藏输入Key,进行最多两次可能计费的模型请求;保存仍需另行输入SAVE。
macOS / Linux:
.venv/bin/python planner.py --request "请把N1和N2安排在2026-10-05复习20分钟,标题为RAG基础复习。只保存本地计划。"
Windows PowerShell:
.\.venv\Scripts\python.exe planner.py --request "请把N1和N2安排在2026-10-05复习20分钟,标题为RAG基础复习。只保存本地计划。"
先把日期改成自己的实际安排。真实输出不保证与演示逐字一致:如果模型认为缺少信息,它可能直接询问,此时只有一次模型请求,状态为 no_tool,不会保存计划。你补充完整请求后再开始新运行,本课还没有交互式追问循环。
真实计划写入 plans,演示计划写入 demo-plans。请求与响应可能包含笔记和安排,分享记录前要检查内容。
一次工具任务可能花两次模型调用的费用。summary.calls 分别保留两次请求的usage和调用耗时,第二次输入还包括工具定义、提议和结果,不能只看最终回答的字数估算开销。演示usage与耗时为null。
第09篇的 costs.py 仍只读取卡片格式的 records/*-result.json。本篇工具记录放在单独的 tool-records,不能把summary硬塞进旧估算器就说获得了整个任务成本。若要手动核对,分别读取每次usage,按第09篇公式计算已知部分;有一次未知,就保留总成本缺口。这里先保留数据,不默默改变旧工具的输入协议。
十、用两个场景检查边界
**场景一:**模型返回合法的四个字段,但日期是你没有要求的下周。程序字段检查通过,你应该输入SAVE吗?
不应该。结构检查只证明这是一项格式合理的计划。应取消,重新说明实际日期;程序不负责猜测你内心真正想安排哪天。
**场景二:**计划文件已存在,第二次模型请求失败。重新运行并再次SAVE能修复这个失败吗?
它可能新增另一份计划,不能自动修复原任务。先打开原文件与summary,确认保存已经完成;本课宁可保留“计划已保存、总结失败”的状态,也不重复执行。
| 现象 | 检查方向 |
|---|---|
| 模型直接问日期,没有工具提议 | auto允许直接回答;补全请求后再运行 |
| 没出现SAVE提示便停止 | 查看工具名、字段、结束状态;不要绕过校验 |
| 输入save却没保存 | 确认词严格为大写SAVE,其他输入均取消 |
| 取消后目录里仍有文件 | 核对本次ID;以前的计划不会被删除 |
| 最后报错但文件存在 | 检查tool_result与第二次调用,不盲目重跑 |
| 模型说设置了提醒,但没有提醒 | 本课仅写本地JSON;以程序功能和文件为准 |
| 旧费用工具看不到计划任务 | 两类记录格式不同,tool-records的calls需分别核对 |
这一步让助手从生成文字迈向受控执行。下一篇回到它所依据的资料:读取自己的文本和Markdown文件,同时保留来源与版本,让复习卡和后续检索有真实、可追溯的输入。
上一篇:用量与缓存 · 本部分大纲 · 代码包 · 运行说明
验证范围:47项离线测试通过,含前篇回归与实际SDK模拟HTTP、工具参数、确认/取消、截断/多调用、写盘失败和执行后请求失败。无真实API或外部日历操作,Windows/Linux命令未实机执行。