<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://ranjian.pages.dev/blog</id>
    <title>染笺的小站 Blog</title>
    <updated>2026-11-02T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://ranjian.pages.dev/blog"/>
    <subtitle>染笺的小站 Blog</subtitle>
    <icon>https://ranjian.pages.dev/img/favicon.svg</icon>
    <entry>
        <title type="html"><![CDATA[Agent 开发环境准备：虚拟环境与解释器]]></title>
        <id>https://ranjian.pages.dev/blog/agent-env-setup</id>
        <link href="https://ranjian.pages.dev/blog/agent-env-setup"/>
        <updated>2026-11-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[准备独立的 Python 虚拟环境, 装依赖, 让 VS Code 和 WSL 都用对解释器, 最后用自检脚本确认环境可用.]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="环境准备">环境准备<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87" class="hash-link" aria-label="环境准备的直接链接" title="环境准备的直接链接" translate="no">​</a></h2>
<p>手写 Agent 之前先把 Python 环境理顺: 一个独立的虚拟环境, 装好依赖, 让编辑器和终端都用对解释器, 最后跑一次自检脚本确认能调通模型.</p>
<blockquote>
<p>📌 下文命令里的 <code>&lt;项目目录&gt;</code> 指你放这个练习的目录, 换成自己的路径即可.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="为什么用独立虚拟环境">为什么用独立虚拟环境<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E4%B8%BA%E4%BB%80%E4%B9%88%E7%94%A8%E7%8B%AC%E7%AB%8B%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="为什么用独立虚拟环境的直接链接" title="为什么用独立虚拟环境的直接链接" translate="no">​</a></h2>
<p>系统全局 Python 里通常装着一堆别的项目的包, <code>openai</code> 这类库的版本会互相牵扯. 项目用独立 venv, <code>requirements.txt</code> 才有意义, 换一台机器也能一次装齐.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="创建虚拟环境">创建虚拟环境<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E5%88%9B%E5%BB%BA%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="创建虚拟环境的直接链接" title="创建虚拟环境的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="windows-环境">Windows 环境<a href="https://ranjian.pages.dev/blog/agent-env-setup#windows-%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="Windows 环境的直接链接" title="Windows 环境的直接链接" translate="no">​</a></h3>
<p>在 PowerShell 里进入代码目录:</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">cd "&lt;项目目录&gt;"</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">python -m venv .venv</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">.\.venv\Scripts\python.exe -m pip install -r requirements.txt</span><br></div></code></pre></div></div>
<p>不激活也能用: 直接调 <code>.venv\Scripts\python.exe</code>, 省得被 PowerShell 执行策略拦. 想激活就用:</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">.\.venv\Scripts\Activate.ps1</span><br></div></code></pre></div></div>
<p>被拦的话先跑一次 <code>Set-ExecutionPolicy -Scope CurrentUser RemoteSigned</code>.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="让-vs-code-用这个解释器">让 VS Code 用这个解释器<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E8%AE%A9-vs-code-%E7%94%A8%E8%BF%99%E4%B8%AA%E8%A7%A3%E9%87%8A%E5%99%A8" class="hash-link" aria-label="让 VS Code 用这个解释器的直接链接" title="让 VS Code 用这个解释器的直接链接" translate="no">​</a></h4>
<p><code>Ctrl + Shift + P</code> → <code>Python: Select Interpreter</code> → 输入解释器路径... → 粘贴 <code>.venv\Scripts\python.exe</code> 的绝对路径.</p>
<p>下拉列表里通常看不到它: venv 埋得比较深, 路径还可能带中文, 自动扫描不会下探这么远. 手动填路径最省事, 选一次就会记住, 右下角状态栏会显示 <code>.venv</code>.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="验证解释器">验证解释器<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E9%AA%8C%E8%AF%81%E8%A7%A3%E9%87%8A%E5%99%A8" class="hash-link" aria-label="验证解释器的直接链接" title="验证解释器的直接链接" translate="no">​</a></h4>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">python -c "import sys; print(sys.executable)"</span><br></div></code></pre></div></div>
<p>输出的路径里要带 <code>.venv\Scripts\python.exe</code>. 显示的是系统 Python 路径, 说明没选对解释器, 或者终端是选之前开着的——新开一个终端再试.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="wsl-环境">WSL 环境<a href="https://ranjian.pages.dev/blog/agent-env-setup#wsl-%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="WSL 环境的直接链接" title="WSL 环境的直接链接" translate="no">​</a></h3>
<p>Windows 的 <code>.venv</code> 和 WSL 的虚拟环境分开使用, 建议把 Linux 环境放到家目录下, 例如 <code>~/.venvs/agent</code>.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="首次创建与安装依赖">首次创建与安装依赖<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E9%A6%96%E6%AC%A1%E5%88%9B%E5%BB%BA%E4%B8%8E%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96" class="hash-link" aria-label="首次创建与安装依赖的直接链接" title="首次创建与安装依赖的直接链接" translate="no">​</a></h4>
<p>以下命令在 Ubuntu 的 Bash 终端执行. 如果创建环境时提示 <code>ensurepip is not available</code>, 先安装虚拟环境组件; 已经装过就跳过:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token function" style="color:rgb(130, 170, 255)">sudo</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">apt-get</span><span class="token plain"> update</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token function" style="color:rgb(130, 170, 255)">sudo</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">apt-get</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">install</span><span class="token plain"> </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">-y</span><span class="token plain"> python3-venv</span><br></div></code></pre></div></div>
<p>进入代码目录, 创建并激活环境, 再安装项目依赖:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token builtin class-name" style="color:rgb(255, 203, 107)">cd</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"&lt;项目目录&gt;"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">python3 </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">-m</span><span class="token plain"> venv ~/.venvs/agent</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token builtin class-name" style="color:rgb(255, 203, 107)">source</span><span class="token plain"> ~/.venvs/agent/bin/activate</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">python </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">-m</span><span class="token plain"> pip </span><span class="token function" style="color:rgb(130, 170, 255)">install</span><span class="token plain"> </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">-r</span><span class="token plain"> requirements.txt</span><br></div></code></pre></div></div>
<p>创建环境和安装依赖通常只做一次; 依赖清单更新后再安装. 关闭终端不会删除虚拟环境或已安装的包.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="每次打开终端后激活">每次打开终端后激活<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E6%AF%8F%E6%AC%A1%E6%89%93%E5%BC%80%E7%BB%88%E7%AB%AF%E5%90%8E%E6%BF%80%E6%B4%BB" class="hash-link" aria-label="每次打开终端后激活的直接链接" title="每次打开终端后激活的直接链接" translate="no">​</a></h4>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token builtin class-name" style="color:rgb(255, 203, 107)">source</span><span class="token plain"> ~/.venvs/agent/bin/activate</span><br></div></code></pre></div></div>
<p>激活只影响当前终端: 它会把虚拟环境的 <code>bin</code> 目录放到 <code>PATH</code> 前面, 让 <code>python</code> 和 <code>pip</code> 优先使用这个环境里的版本. 新终端需要重新激活.</p>
<blockquote>
<p>📌 文件名是 <code>activate</code>, 最后有一个 <code>e</code>. 写成 <code>activat</code> 会报 <code>No such file or directory</code>, 环境也不会激活.</p>
</blockquote>
<p>验证当前解释器:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">python </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">-c</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">'import sys; print(sys.executable)'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">python </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">-m</span><span class="token plain"> pip </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">--version</span><br></div></code></pre></div></div>
<p>路径应指向 <code>~/.venvs/agent/bin/python</code> 对应的绝对路径. 如果显示 <code>/usr/bin/python3</code>, 说明还在用系统 Python; 激活成功后提示符通常也会显示 <code>(agent)</code>.</p>
<h4 class="anchor anchorTargetStickyNavbar_Vzrq" id="退出与直接运行">退出与直接运行<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E9%80%80%E5%87%BA%E4%B8%8E%E7%9B%B4%E6%8E%A5%E8%BF%90%E8%A1%8C" class="hash-link" aria-label="退出与直接运行的直接链接" title="退出与直接运行的直接链接" translate="no">​</a></h4>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">deactivate</span><br></div></code></pre></div></div>
<p>退出只取消当前终端的激活状态, 不删除环境. 也可以不激活, 直接指定虚拟环境里的解释器:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">~/.venvs/agent/bin/python </span><span class="token parameter variable" style="color:rgb(191, 199, 213)">-c</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">'import sys; print(sys.executable)'</span><br></div></code></pre></div></div>
<p>命令格式固定, 换项目时改环境路径即可. 创建用的是环境目录 <code>~/.venvs/agent</code>, 激活用的是目录中的脚本 <code>~/.venvs/agent/bin/activate</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="检查环境">检查环境<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E6%A3%80%E6%9F%A5%E7%8E%AF%E5%A2%83" class="hash-link" aria-label="检查环境的直接链接" title="检查环境的直接链接" translate="no">​</a></h2>
<p>Windows 终端:</p>
<div class="language-powershell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-powershell codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">cd "&lt;项目目录&gt;"</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">python 00-check-env.py</span><br></div></code></pre></div></div>
<p>WSL 终端:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token builtin class-name" style="color:rgb(255, 203, 107)">source</span><span class="token plain"> ~/.venvs/agent/bin/activate</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token builtin class-name" style="color:rgb(255, 203, 107)">cd</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"&lt;项目目录&gt;"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">python 00-check-env.py</span><br></div></code></pre></div></div>
<p>正常输出分几段, 逐段对:</p>
<table><thead><tr><th style="text-align:center">输出</th><th style="text-align:center">表示</th></tr></thead><tbody><tr><td style="text-align:center"><code>BASE_URL = https://api.deepseek.com</code></td><td style="text-align:center">地址对了</td></tr><tr><td style="text-align:center"><code>MODEL    = deepseek-flash</code></td><td style="text-align:center">模型名对了</td></tr><tr><td style="text-align:center"><code>连接成功，可用模型：[...]</code></td><td style="text-align:center">网络通, 凭证可用</td></tr><tr><td style="text-align:center"><code>对话成功：'通了'</code></td><td style="text-align:center">真的能出结果</td></tr></tbody></table>
<p>看到「环境没问题」就算过了.</p>
<blockquote>
<p>💡 每跑一次会真实调用一次模型, 费用可以忽略, 但别写成循环反复跑.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="常见报错">常见报错<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E5%B8%B8%E8%A7%81%E6%8A%A5%E9%94%99" class="hash-link" aria-label="常见报错的直接链接" title="常见报错的直接链接" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">现象</th><th style="text-align:center">原因</th><th style="text-align:center">处理</th></tr></thead><tbody><tr><td style="text-align:center"><code>ensurepip is not available</code></td><td style="text-align:center">WSL 缺少虚拟环境组件</td><td style="text-align:center">安装 <code>python3-venv</code> 后重新创建环境</td></tr><tr><td style="text-align:center">激活时报 <code>No such file or directory</code></td><td style="text-align:center">路径或 <code>activate</code> 拼写错误</td><td style="text-align:center">核对路径, 确认末尾有 <code>e</code></td></tr><tr><td style="text-align:center">WSL 提示 <code>python</code> 找不到</td><td style="text-align:center">虚拟环境未激活</td><td style="text-align:center">先执行 <code>source</code> 激活命令</td></tr><tr><td style="text-align:center"><code>/usr/bin/python3: No module named pip</code></td><td style="text-align:center">调用了系统 Python</td><td style="text-align:center">激活后用 <code>python -m pip</code></td></tr><tr><td style="text-align:center"><code>Invalid port: ':1]'</code></td><td style="text-align:center"><code>NO_PROXY</code> 里有 <code>[::1]</code></td><td style="text-align:center">去掉方括号再试</td></tr><tr><td style="text-align:center"><code>APIConnectionError</code></td><td style="text-align:center">代理没开或网络不通</td><td style="text-align:center">确认代理端口在跑</td></tr><tr><td style="text-align:center">提示 MODEL 不在列表里</td><td style="text-align:center">模型名写错</td><td style="text-align:center">改成 <code>deepseek-flash</code></td></tr></tbody></table>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="收工自检">收工自检<a href="https://ranjian.pages.dev/blog/agent-env-setup#%E6%94%B6%E5%B7%A5%E8%87%AA%E6%A3%80" class="hash-link" aria-label="收工自检的直接链接" title="收工自检的直接链接" translate="no">​</a></h2>
<ul class="contains-task-list containsTaskList_mC6p">
<li class="task-list-item"><input type="checkbox" disabled=""> <!-- -->解释器指向当前系统的独立环境: Windows 的 <code>.venv</code> 或 WSL 的 <code>~/.venvs/agent</code></li>
<li class="task-list-item"><input type="checkbox" disabled=""> <code>python 00-check-env.py</code> 输出「环境没问题」</li>
<li class="task-list-item"><input type="checkbox" disabled=""> <code>git status</code> 里看不到 <code>.venv</code></li>
</ul>
<p><em>更新记录: 2026-11-02 发布版, 去掉本地路径和凭证配置细节, 保留虚拟环境, 解释器选择与自检部分.</em></p>]]></content>
        <category label="Agent" term="Agent"/>
        <category label="Python" term="Python"/>
        <category label="环境配置" term="环境配置"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[手写 Agent 第 1 课：最小循环]]></title>
        <id>https://ranjian.pages.dev/blog/agent-minimal-loop</id>
        <link href="https://ranjian.pages.dev/blog/agent-minimal-loop"/>
        <updated>2026-11-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[跑通一次完整的工具调用循环: 请求模型, 执行工具, 回填结果, 再请求模型, 直到模型不再请求工具.]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第-1-课最小循环">第 1 课：最小循环<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E7%AC%AC-1-%E8%AF%BE%E6%9C%80%E5%B0%8F%E5%BE%AA%E7%8E%AF" class="hash-link" aria-label="第 1 课：最小循环的直接链接" title="第 1 课：最小循环的直接链接" translate="no">​</a></h2>
<p>本课已完成手写与断点调试。核心是：<strong>模型提出工具调用请求，Python 执行工具，把结果追加到 <code>messages</code>，再请求模型；没有工具调用请求时结束，达到轮数上限时强制停止。</strong></p>
<p>自己的实现见 <code>code/01-minimal-loop.py</code>，环境配置见 <a class="" href="https://ranjian.pages.dev/blog/agent-env-setup">环境准备</a>，参考教程见 NamiAgent 第 1 课。本文按当前代码整理，重点记录返回结构和消息流转。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="工具与模型的分工">工具与模型的分工<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E5%B7%A5%E5%85%B7%E4%B8%8E%E6%A8%A1%E5%9E%8B%E7%9A%84%E5%88%86%E5%B7%A5" class="hash-link" aria-label="工具与模型的分工的直接链接" title="工具与模型的分工的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="三个工具与两份数据">三个工具与两份数据<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E4%B8%89%E4%B8%AA%E5%B7%A5%E5%85%B7%E4%B8%8E%E4%B8%A4%E4%BB%BD%E6%95%B0%E6%8D%AE" class="hash-link" aria-label="三个工具与两份数据的直接链接" title="三个工具与两份数据的直接链接" translate="no">​</a></h3>
<p>工具就是普通的 Python 函数。模型拿到工具说明后，生成“调用哪个函数、传什么参数”的结构化请求；真正读文件、写文件和执行命令的是本地 Python 代码。</p>
<table><thead><tr><th style="text-align:center">工具</th><th style="text-align:center">参数</th><th style="text-align:center">用途</th></tr></thead><tbody><tr><td style="text-align:center"><code>read_file</code></td><td style="text-align:center"><code>path</code></td><td style="text-align:center">读取文件内容</td></tr><tr><td style="text-align:center"><code>write_file</code></td><td style="text-align:center"><code>path</code>、<code>content</code></td><td style="text-align:center">写入文件，覆盖原内容</td></tr><tr><td style="text-align:center"><code>run_shell</code></td><td style="text-align:center"><code>command</code></td><td style="text-align:center">执行命令并返回输出</td></tr></tbody></table>
<p>当前实现中，有两份名字相近、用途不同的数据：</p>
<ul>
<li class=""><code>TOOLS</code>：工具说明列表，传给模型，包含名称、描述和参数结构。<code>def_tools()</code> 负责生成这些说明。</li>
<li class=""><code>TOOL_MAP</code>：函数映射字典，留在本地，根据模型返回的工具名找到 Python 函数。</li>
</ul>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">TOOL_MAP </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string" style="color:rgb(195, 232, 141)">"read_file"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> read_file</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string" style="color:rgb(195, 232, 141)">"write_file"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> write_file</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string" style="color:rgb(195, 232, 141)">"run_shell"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> run_shell</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>左边的 <code>"write_file"</code> 是字符串键，右边的 <code>write_file</code> 是函数对象。没有加调用括号，所以定义字典时不会执行函数。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="什么任务容易触发工具调用">什么任务容易触发工具调用<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E4%BB%80%E4%B9%88%E4%BB%BB%E5%8A%A1%E5%AE%B9%E6%98%93%E8%A7%A6%E5%8F%91%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8" class="hash-link" aria-label="什么任务容易触发工具调用的直接链接" title="什么任务容易触发工具调用的直接链接" translate="no">​</a></h3>
<p>“给我一段打印 hello 的代码”通常直接回复文字就能完成。“实际创建文件，再读取并运行它”则需要操作本地环境。</p>
<p>当前代码末尾的任务要求依次使用 <code>write_file</code>、<code>read_file</code>、<code>run_shell</code>：创建 <code>tool_demo.py</code>，写入 <code>print(17 * 23)</code>，读回检查，再运行并总结实际输出。</p>
<p>这只是预期流程。模型可能一轮请求多个工具，也可能分几轮请求。后一个操作需要前一个工具的实际结果时，通常需要下一轮才能作出判断。</p>
<blockquote>
<p>📌 当前 <code>01-minimal-loop.py</code> 在末尾定义 <code>task</code>，然后执行 <code>run(task)</code>。修改任务要改这个字符串；当前入口没有读取 <code>sys.argv[1]</code>，在启动命令后追加任务文本不会替换它。</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="create-的返回结构">create 的返回结构<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#create-%E7%9A%84%E8%BF%94%E5%9B%9E%E7%BB%93%E6%9E%84" class="hash-link" aria-label="create 的返回结构的直接链接" title="create 的返回结构的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="这次调用发出了什么">这次调用发出了什么<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E8%BF%99%E6%AC%A1%E8%B0%83%E7%94%A8%E5%8F%91%E5%87%BA%E4%BA%86%E4%BB%80%E4%B9%88" class="hash-link" aria-label="这次调用发出了什么的直接链接" title="这次调用发出了什么的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">resp </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">chat</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">completions</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">create</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    model</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain">MODEL</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    messages</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain">messages</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    tools</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain">TOOLS</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>三个参数分别表示：</p>
<ul>
<li class=""><code>model</code>：使用哪个模型。代码中的默认值是 <code>deepseek-flash</code>，启动前可以调整。</li>
<li class=""><code>messages</code>：到目前为止的对话，包括任务、模型回复和已有工具结果。</li>
<li class=""><code>tools</code>：本轮向模型提供的工具说明。</li>
</ul>
<p>当前没有设置 <code>stream=True</code>，所以拿到的是一次完整的非流式响应。虽然使用 OpenAI Python SDK，当前默认请求地址是 DeepSeek；SDK 将兼容接口返回的 JSON 解析成 <code>ChatCompletion</code> 对象，赋值给 <code>resp</code>。</p>
<p><strong><code>resp</code> 包含本轮模型响应，不是完整对话历史，也不是工具已经执行后的结果。</strong> 工具请求返回后，还需要下面的 Python 循环来执行。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="情况一直接回复文字">情况一：直接回复文字<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E6%83%85%E5%86%B5%E4%B8%80%E7%9B%B4%E6%8E%A5%E5%9B%9E%E5%A4%8D%E6%96%87%E5%AD%97" class="hash-link" aria-label="情况一：直接回复文字的直接链接" title="情况一：直接回复文字的直接链接" translate="no">​</a></h3>
<p>下面是用于阅读结构的简化 JSON，编号和正文均为示例，不是本次调试的真实记录。省略了 <code>created</code>、<code>usage</code> 等字段；实际响应还可能包含服务商或模型相关字段。</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"chatcmpl_text_demo"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"object"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"chat.completion"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"model"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"deepseek-flash"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"choices"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"index"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"message"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"role"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"assistant"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"content"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"你好！有什么需要我帮忙的吗？"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"tool_calls"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token null keyword" style="font-style:italic">null</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"finish_reason"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"stop"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p><code>message.content</code> 是正文。这个例子没有工具调用，所以 <code>tool_calls</code> 为 <code>null</code>，在 Python 中对应 <code>None</code>。实际响应也可能省略这个可选字段，SDK 中通常仍能读到 <code>None</code>。</p>
<p><code>finish_reason</code> 属于 <code>choices[0]</code>，说明本轮生成为什么结束。这里的 <code>stop</code> 表示正常结束生成；它本身不证明外部任务已经完成。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="情况二请求调用工具">情况二：请求调用工具<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E6%83%85%E5%86%B5%E4%BA%8C%E8%AF%B7%E6%B1%82%E8%B0%83%E7%94%A8%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="情况二：请求调用工具的直接链接" title="情况二：请求调用工具的直接链接" translate="no">​</a></h3>
<p>假设任务是“在当前目录创建 <code>tool_note.txt</code>，内容为 <code>hello</code>”，模型可能返回：</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"chatcmpl_tool_demo"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"object"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"chat.completion"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"model"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"deepseek-flash"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"choices"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"index"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"message"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"role"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"assistant"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"content"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token null keyword" style="font-style:italic">null</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"tool_calls"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">          </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            </span><span class="token property">"id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"call_write_001"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            </span><span class="token property">"type"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"function"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            </span><span class="token property">"function"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              </span><span class="token property">"name"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"write_file"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              </span><span class="token property">"arguments"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"{\"path\":\"tool_note.txt\",\"content\":\"hello\"}"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">          </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token property">"finish_reason"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool_calls"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>这个例子表达的是：请调用 <code>write_file</code>，参数是文件路径和文件内容。<strong>看到这个返回时，文件还没有被本地代码写入。</strong></p>
<p>重点看这些字段：</p>
<table><thead><tr><th style="text-align:center">字段</th><th style="text-align:center">含义</th></tr></thead><tbody><tr><td style="text-align:center"><code>choices</code></td><td style="text-align:center">候选回复列表</td></tr><tr><td style="text-align:center"><code>choices[0]</code></td><td style="text-align:center">第一个候选回复</td></tr><tr><td style="text-align:center"><code>message</code></td><td style="text-align:center">该候选中的助手消息</td></tr><tr><td style="text-align:center"><code>message.content</code></td><td style="text-align:center">正文，可能为 <code>None</code></td></tr><tr><td style="text-align:center"><code>message.tool_calls</code></td><td style="text-align:center">本轮工具调用请求列表</td></tr><tr><td style="text-align:center"><code>tool_calls[0].id</code></td><td style="text-align:center">第一条工具调用的编号</td></tr><tr><td style="text-align:center"><code>function.name</code></td><td style="text-align:center">要调用的工具名</td></tr><tr><td style="text-align:center"><code>function.arguments</code></td><td style="text-align:center">JSON 格式的参数字符串</td></tr><tr><td style="text-align:center"><code>finish_reason</code></td><td style="text-align:center">本轮生成结束的原因</td></tr></tbody></table>
<p><code>choices[0]</code> 取的是本轮第一个候选回复，不是 <code>messages</code> 中第一条历史消息。<code>tool_calls[0]</code> 才是这条助手消息中的第一个工具调用请求。</p>
<p>工具请求可以带有文字正文，所以不能只靠 <code>content</code> 是否为空判断是否需要工具。当前实现检查的是 <code>msg.tool_calls</code>。返回字段依据见 <a href="https://api-docs.deepseek.com/api/create-chat-completion/" target="_blank" rel="noopener noreferrer" class="">DeepSeek Chat Completions 文档</a>。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="怎样看见自己的真实返回">怎样看见自己的真实返回<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E6%80%8E%E6%A0%B7%E7%9C%8B%E8%A7%81%E8%87%AA%E5%B7%B1%E7%9A%84%E7%9C%9F%E5%AE%9E%E8%BF%94%E5%9B%9E" class="hash-link" aria-label="怎样看见自己的真实返回的直接链接" title="怎样看见自己的真实返回的直接链接" translate="no">​</a></h3>
<p>在 <code>create()</code> 后面临时打印：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token builtin" style="color:rgb(130, 170, 255)">type</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">resp</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">resp</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">model_dump_json</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">indent</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token number" style="color:rgb(247, 140, 108)">2</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">msg </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">choices</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">message</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">model_dump_json</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">indent</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token number" style="color:rgb(247, 140, 108)">2</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p><code>model_dump_json()</code> 将 SDK 对象转成 JSON 字符串；<code>indent=2</code> 让每层缩进两个空格。这是 SDK 解析后的响应视图。</p>
<p>也可以转成字典后访问，两种写法对应同一份数据：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">data </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">model_dump</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">resp</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">choices</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">message</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">content</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain">             </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 对象访问</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">data</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"choices"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"message"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string" style="color:rgb(195, 232, 141)">"content"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain">     </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 字典访问</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="从响应取出工具调用">从响应取出工具调用<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E4%BB%8E%E5%93%8D%E5%BA%94%E5%8F%96%E5%87%BA%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8" class="hash-link" aria-label="从响应取出工具调用的直接链接" title="从响应取出工具调用的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="逐层理解取值路径">逐层理解取值路径<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E9%80%90%E5%B1%82%E7%90%86%E8%A7%A3%E5%8F%96%E5%80%BC%E8%B7%AF%E5%BE%84" class="hash-link" aria-label="逐层理解取值路径的直接链接" title="逐层理解取值路径的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">msg </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">choices</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">message</span><br></div></code></pre></div></div>
<p>拆开后是：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">choices </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> resp</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">choices  </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 取出候选回复列表</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">choice </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> choices</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain">    </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 取出第一个候选回复</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">msg </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> choice</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">message  </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 取出助手消息</span><br></div></code></pre></div></div>
<p>SDK 对象用点号取属性，列表用下标取元素。拿到 <code>msg</code> 后，再遍历它请求的工具：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">for</span><span class="token plain"> call </span><span class="token keyword" style="font-style:italic">in</span><span class="token plain"> msg</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">tool_calls</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    name </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">function</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">name</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    args </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> json</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">loads</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">function</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">arguments</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    result </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">TOOL_MAP</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token operator" style="color:rgb(137, 221, 255)">**</span><span class="token plain">args</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>这里的 <code>call</code> 是列表里当前遍历到的一条调用请求。实际代码在进入循环前，已经通过 <code>if not msg.tool_calls</code> 排除了没有工具请求的情况。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="arguments-为什么还要解析">arguments 为什么还要解析<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#arguments-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%98%E8%A6%81%E8%A7%A3%E6%9E%90" class="hash-link" aria-label="arguments 为什么还要解析的直接链接" title="arguments 为什么还要解析的直接链接" translate="no">​</a></h3>
<p>示例中的 <code>arguments</code> 看起来像一个对象，但外面有引号，它实际是字符串。内部的 <code>\"</code> 是 JSON 字符串里的双引号转义。</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">raw </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">function</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">arguments</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># raw 是字符串：'{"path":"tool_note.txt","content":"hello"}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">args </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> json</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">loads</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">raw</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># args 是字典：{"path": "tool_note.txt", "content": "hello"}</span><br></div></code></pre></div></div>
<p>解析后的字典，用 JSON 表示就是：</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"path"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool_note.txt"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"content"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"hello"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>外层 SDK 对象已经被解析，不代表 <code>arguments</code> 这个字段里的 JSON 字符串也会自动变成字典。因此这里还需要 <code>json.loads()</code>。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="根据名字执行本地函数">根据名字执行本地函数<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E6%A0%B9%E6%8D%AE%E5%90%8D%E5%AD%97%E6%89%A7%E8%A1%8C%E6%9C%AC%E5%9C%B0%E5%87%BD%E6%95%B0" class="hash-link" aria-label="根据名字执行本地函数的直接链接" title="根据名字执行本地函数的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">result </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">TOOL_MAP</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token operator" style="color:rgb(137, 221, 255)">**</span><span class="token plain">args</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>这句可以拆成三步：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">func </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> TOOL_MAP</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain">  </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 根据工具名取出函数</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">value </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> func</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token operator" style="color:rgb(137, 221, 255)">**</span><span class="token plain">args</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain">   </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 展开参数并执行函数</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">result </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">value</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain">   </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 把函数返回值转成字符串</span><br></div></code></pre></div></div>
<p>使用上面的写文件参数时，相当于：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">result </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">write_file</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">path</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token string" style="color:rgb(195, 232, 141)">"tool_note.txt"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> content</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token string" style="color:rgb(195, 232, 141)">"hello"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p><code>**args</code> 把字典展开成关键字参数，字典的键需要匹配函数参数名。这里传入的是 <code>path</code> 和 <code>content</code> 两个参数。</p>
<p>它与 Java 反射都有“运行时选择调用目标”的使用场景；这里的机制更接近在 <code>Map</code> 中保存函数式接口，取出来后调用，不需要通过类元数据查找方法。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="messages-如何记录过程">messages 如何记录过程<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#messages-%E5%A6%82%E4%BD%95%E8%AE%B0%E5%BD%95%E8%BF%87%E7%A8%8B" class="hash-link" aria-label="messages 如何记录过程的直接链接" title="messages 如何记录过程的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="每条消息是谁写的">每条消息是谁写的<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E6%AF%8F%E6%9D%A1%E6%B6%88%E6%81%AF%E6%98%AF%E8%B0%81%E5%86%99%E7%9A%84" class="hash-link" aria-label="每条消息是谁写的的直接链接" title="每条消息是谁写的的直接链接" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:center">消息角色</th><th style="text-align:center">谁生成内容</th><th style="text-align:center">记录什么</th><th style="text-align:center">缺少后的影响</th></tr></thead><tbody><tr><td style="text-align:center"><code>system</code></td><td style="text-align:center">开发者</td><td style="text-align:center">助手职责和行为要求</td><td style="text-align:center">少了这层行为约束</td></tr><tr><td style="text-align:center"><code>user</code></td><td style="text-align:center">用户，本例为 <code>task</code></td><td style="text-align:center">本次任务</td><td style="text-align:center">没有本次任务内容</td></tr><tr><td style="text-align:center"><code>assistant</code>，带工具请求</td><td style="text-align:center">模型</td><td style="text-align:center">工具名、参数和调用编号</td><td style="text-align:center">工具结果缺少对应请求</td></tr><tr><td style="text-align:center"><code>tool</code></td><td style="text-align:center">Python 工具执行代码</td><td style="text-align:center">执行结果或错误信息</td><td style="text-align:center">模型不知道执行结果</td></tr><tr><td style="text-align:center"><code>assistant</code>，文字回复</td><td style="text-align:center">模型</td><td style="text-align:center">回答或任务总结</td><td style="text-align:center">少了这条回复记录</td></tr></tbody></table>
<p>这些消息都由 Python 代码放入 <code>messages</code>。其中，模型生成的助手消息由 SDK 返回，工具结果由本地函数返回。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="先记请求再记结果">先记请求，再记结果<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E5%85%88%E8%AE%B0%E8%AF%B7%E6%B1%82%E5%86%8D%E8%AE%B0%E7%BB%93%E6%9E%9C" class="hash-link" aria-label="先记请求，再记结果的直接链接" title="先记请求，再记结果的直接链接" translate="no">​</a></h3>
<p>拿到助手消息后，先追加它：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">messages</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">model_dump</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">exclude_none</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token boolean" style="color:rgb(255, 88, 116)">True</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p><code>model_dump()</code> 把 SDK 消息对象转成字典，<code>exclude_none=True</code> 表示省略值为 <code>None</code> 的字段。追加的是 <code>msg</code>，不是包含 <code>choices</code> 等信息的完整 <code>resp</code>。</p>
<p>执行每一个工具后，再追加对应结果：</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">messages</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string" style="color:rgb(195, 232, 141)">"role"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string" style="color:rgb(195, 232, 141)">"tool_call_id"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token builtin" style="color:rgb(130, 170, 255)">id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string" style="color:rgb(195, 232, 141)">"content"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> result</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p><code>tool_call_id</code> 必须匹配原调用请求的 <code>id</code>。它用来告诉模型：这条结果属于哪一次调用，而不是只按工具名关联。</p>
<p>下面展示一次写文件再总结的简化消息历史。为突出结构，省略了实际模型可能返回的其他消息字段：</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"role"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"system"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"content"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"你是一个能操作本地文件和命令行的编程助手。需要时调用工具，完成任务后直接用文字总结。"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"role"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"user"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"content"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"请在当前目录创建 tool_note.txt，内容为 hello。"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"role"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"assistant"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"tool_calls"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"call_write_001"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"type"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"function"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token property">"function"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">          </span><span class="token property">"name"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"write_file"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">          </span><span class="token property">"arguments"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"{\"path\":\"tool_note.txt\",\"content\":\"hello\"}"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"role"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"tool_call_id"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"call_write_001"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"content"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"已写入 tool_note.txt（5 字符）"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"role"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"assistant"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"content"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"已创建 tool_note.txt，写入内容为 hello。"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><br></div></code></pre></div></div>
<p>第五条消息来自工具执行后的下一轮模型响应。模型会看到前四条记录，再根据执行结果生成总结。这段 JSON 是说明用的示例，不代表程序已经实际执行了写文件操作。</p>
<p>如果同一条助手消息请求了多个工具，代码要执行并记录每个请求的结果，再进入下一轮模型请求。当前内层 <code>for</code> 是顺序执行，没有并发执行工具。</p>
<blockquote>
<p>📌 工具报错也要回填。当前 <code>try/except</code> 会把文件不存在、参数错误等异常转成文字，作为 <code>tool</code> 消息交给模型；下一轮模型可能修正参数，也可能解释失败后结束。</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="循环何时继续和停止">循环何时继续和停止<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E5%BE%AA%E7%8E%AF%E4%BD%95%E6%97%B6%E7%BB%A7%E7%BB%AD%E5%92%8C%E5%81%9C%E6%AD%A2" class="hash-link" aria-label="循环何时继续和停止的直接链接" title="循环何时继续和停止的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="两种正常停止路径">两种正常停止路径<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E4%B8%A4%E7%A7%8D%E6%AD%A3%E5%B8%B8%E5%81%9C%E6%AD%A2%E8%B7%AF%E5%BE%84" class="hash-link" aria-label="两种正常停止路径的直接链接" title="两种正常停止路径的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">for</span><span class="token plain"> step </span><span class="token keyword" style="font-style:italic">in</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">range</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token number" style="color:rgb(247, 140, 108)">1</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> MAX_STEPS </span><span class="token operator" style="color:rgb(137, 221, 255)">+</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">1</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 请求模型，记录助手消息</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">if</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">not</span><span class="token plain"> msg</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">tool_calls</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">msg</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">content</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># 执行本轮全部工具，记录结果，进入下一轮</span><br></div></code></pre></div></div>
<p>这是省略了请求与执行细节的流程片段。正常控制流程中有两种停止路径：</p>
<ul>
<li class="">模型本轮没有请求工具：<code>if not msg.tool_calls</code> 成立，输出正文，通过 <code>return</code> 退出 <code>run()</code>。</li>
<li class="">模型持续请求工具：最多完成 <code>MAX_STEPS</code> 轮请求，循环结束后输出达到上限的信息。当前上限为 20。</li>
</ul>
<p><code>range(1, MAX_STEPS + 1)</code> 在这里产生 1 到 20，不包含 21。<strong>上限限制的是模型请求轮数，不是工具执行总次数。</strong> 一轮可以包含多个工具请求，最后一轮纯文字总结也占一轮。</p>
<p>工具返回结果并追加到 <code>messages</code>，不会自动再次调用模型。真正让流程继续的是外层 <code>for</code> 的下一次迭代，以及下一次 <code>create()</code>。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="停止不等于验证成功">停止不等于验证成功<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E5%81%9C%E6%AD%A2%E4%B8%8D%E7%AD%89%E4%BA%8E%E9%AA%8C%E8%AF%81%E6%88%90%E5%8A%9F" class="hash-link" aria-label="停止不等于验证成功的直接链接" title="停止不等于验证成功的直接链接" translate="no">​</a></h3>
<p>当前代码的结束条件是“没有工具调用请求”，不是“已验证任务成功”。模型解释失败后也可能不再请求工具；达到上限则表示流程被强制停止。</p>
<p><code>finish_reason="length"</code> 等情况还可能表示生成被截断。当前最小实现没有按 <code>finish_reason</code> 进一步区分这些情况；它仍然只检查 <code>msg.tool_calls</code>。网络或鉴权等模型请求异常也没有被工具的 <code>try/except</code> 捕获，会直接中断程序。</p>
<p>本课先理解这个边界，不把最小循环的“停止”当成完整的业务验收。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="断点调试">断点调试<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95" class="hash-link" aria-label="断点调试的直接链接" title="断点调试的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="重点查看的位置">重点查看的位置<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E9%87%8D%E7%82%B9%E6%9F%A5%E7%9C%8B%E7%9A%84%E4%BD%8D%E7%BD%AE" class="hash-link" aria-label="重点查看的位置的直接链接" title="重点查看的位置的直接链接" translate="no">​</a></h3>
<p>在 <code>msg = resp.choices[0].message</code> 的下一行设置断点。暂停时模型请求已经返回，<code>resp</code> 和 <code>msg</code> 都有值，可以依次展开：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">resp</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  choices</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    [0]</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">      message</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        content</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        tool_calls</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">          [0]</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            id</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            function</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              name</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">              arguments</span><br></div></code></pre></div></div>
<p>在工具执行后的 <code>messages.append(...)</code> 下一行再设置断点，查看新追加的 <code>tool</code> 消息，以及它的 <code>tool_call_id</code> 是否对应原请求的 <code>id</code>。</p>
<p>调试器还会显示 SDK、Pydantic 和 Python 使用的内部属性。<code>_xxx</code> 通常约定供内部使用，<code>__xxx__</code> 通常是特殊属性；本课先关注上面的消息与工具字段。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="工作目录影响工具操作">工作目录影响工具操作<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E5%B7%A5%E4%BD%9C%E7%9B%AE%E5%BD%95%E5%BD%B1%E5%93%8D%E5%B7%A5%E5%85%B7%E6%93%8D%E4%BD%9C" class="hash-link" aria-label="工作目录影响工具操作的直接链接" title="工作目录影响工具操作的直接链接" translate="no">​</a></h3>
<p>文件读写和命令执行没有显式切换目录，所以相对路径以 Python 进程的当前工作目录为准。运行或调试时，将工作目录设为 项目里的 <code>playground</code> 目录，不要让示例文件落到仓库根目录。</p>
<p><code>run_shell</code> 每次执行命令前要求输入 <code>y/n</code>；当前 <code>write_file</code> 和 <code>read_file</code> 没有这一步。命令被拒绝时，返回的“用户拒绝执行该命令”也会作为工具结果进入下一轮。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="本课实现记录">本课实现记录<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E6%9C%AC%E8%AF%BE%E5%AE%9E%E7%8E%B0%E8%AE%B0%E5%BD%95" class="hash-link" aria-label="本课实现记录的直接链接" title="本课实现记录的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="与参考代码的差异">与参考代码的差异<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E4%B8%8E%E5%8F%82%E8%80%83%E4%BB%A3%E7%A0%81%E7%9A%84%E5%B7%AE%E5%BC%82" class="hash-link" aria-label="与参考代码的差异的直接链接" title="与参考代码的差异的直接链接" translate="no">​</a></h3>
<p>以下差异按当前 <code>code/01-minimal-loop.py</code> 与 NamiAgent 的 <code>lesson-01/agent.py</code> 核对：</p>
<ul>
<li class="">配置读取：自己的实现通过 <code>code/model_client.py</code> 统一加载配置文件，导入 <code>MODEL</code> 并调用 <code>create_client()</code>。参考代码直接在文件内创建客户端。</li>
<li class="">工具说明与分发：自己的函数名是 <code>def_tools()</code>，映射名是 <code>TOOL_MAP</code>；参考代码分别叫 <code>_tool()</code> 和 <code>TOOL_FUNCS</code>。当前两种说明构造函数都把简写的参数说明展开成字符串参数的 JSON Schema。</li>
<li class="">工具返回值：自己的实现使用 <code>str(...)</code> 将函数返回值转成字符串，再放入 <code>tool.content</code>；参考实现直接使用工具返回值。</li>
<li class="">任务入口：自己的实现把三工具任务写在文件末尾并调用 <code>run(task)</code>；参考实现通过 <code>if __name__ == "__main__"</code> 读取命令行参数。</li>
</ul>
<p>外层模型请求循环、内层工具执行循环、助手消息与工具消息回填，以及轮数上限的核心逻辑一致。</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="参考资料">参考资料<a href="https://ranjian.pages.dev/blog/agent-minimal-loop#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" class="hash-link" aria-label="参考资料的直接链接" title="参考资料的直接链接" translate="no">​</a></h3>
<ul>
<li class=""><a href="https://api-docs.deepseek.com/api/create-chat-completion/" target="_blank" rel="noopener noreferrer" class="">DeepSeek Chat Completions API</a>：本项目默认服务商的请求参数与返回字段。</li>
<li class=""><a href="https://developers.openai.com/api/reference/python/resources/chat/subresources/completions/methods/create" target="_blank" rel="noopener noreferrer" class="">OpenAI Python SDK 的 Chat Completions 参考</a>：SDK 的返回类型与兼容接口结构。</li>
</ul>
<p><em>更新记录：2026-11-01 完成本课复习笔记，补充返回 JSON、取值路径、工具执行、消息回填和停止条件；同日按课编号更新脚本路径；示例数据不代表新的真实模型调用。</em></p>]]></content>
        <category label="Agent" term="Agent"/>
        <category label="Python" term="Python"/>
        <category label="工具调用" term="工具调用"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[手写 Agent 第 2 课：工具调用]]></title>
        <id>https://ranjian.pages.dev/blog/agent-tools</id>
        <link href="https://ranjian.pages.dev/blog/agent-tools"/>
        <updated>2026-11-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[用装饰器加注册表改造工具系统, 新增工具只写一个函数; 统一执行器把工具报错变成模型能读到的结果.]]></summary>
        <content type="html"><![CDATA[<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="第-2-课-工具调用">第 2 课: 工具调用<a href="https://ranjian.pages.dev/blog/agent-tools#%E7%AC%AC-2-%E8%AF%BE-%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8" class="hash-link" aria-label="第 2 课: 工具调用的直接链接" title="第 2 课: 工具调用的直接链接" translate="no">​</a></h2>
<p>第 1 课的三个工具是写死的. 本课把它改成注册表加统一执行器: 新增工具只写一个函数, 主循环一行不改; 工具报错不再让程序崩, 而是变成模型能读到的一条结果.</p>
<p>代码见 <code>code/02-tools/tools.py</code>, 参考教程见 NamiAgent 第 2 课, 上一课见 <a class="" href="https://ranjian.pages.dev/blog/agent-minimal-loop">最小循环</a>.</p>
<blockquote>
<p>📌 本课代码是单个文件, 工具定义和主循环都在 <code>tools.py</code> 里. 参考教程拆成 <code>tools.py</code> 和 <code>agent.py</code> 两个文件, 差异见文末.</p>
<p>下文代码块均为节选, 省略了函数注释和部分类型标注; 字符串和标点与源文件一致, 完整代码以 <code>code/02-tools/tools.py</code> 为准.</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="第-1-课留下的三个问题">第 1 课留下的三个问题<a href="https://ranjian.pages.dev/blog/agent-tools#%E7%AC%AC-1-%E8%AF%BE%E7%95%99%E4%B8%8B%E7%9A%84%E4%B8%89%E4%B8%AA%E9%97%AE%E9%A2%98" class="hash-link" aria-label="第 1 课留下的三个问题的直接链接" title="第 1 课留下的三个问题的直接链接" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:center">问题</th><th style="text-align:center">第 1 课的做法</th><th style="text-align:center">后果</th></tr></thead><tbody><tr><td style="text-align:center">加工具要改三处</td><td style="text-align:center">写函数, 写说明, 登记 <code>TOOL_MAP</code></td><td style="text-align:center">漏一处就跑不通</td></tr><tr><td style="text-align:center">说明和函数分开写</td><td style="text-align:center"><code>def_tools(...)</code> 和 <code>def</code> 各写一遍</td><td style="text-align:center">改了函数容易忘改说明</td></tr><tr><td style="text-align:center">错误处理堆在循环里</td><td style="text-align:center">循环里一个 <code>try/except</code> 包住所有工具</td><td style="text-align:center">工具越多分支越乱</td></tr></tbody></table>
<p>本课用两个东西解决: 装饰器让"说明跟着函数走", 执行器让"错误统一变成结果".</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="一个工具由什么组成">一个工具由什么组成<a href="https://ranjian.pages.dev/blog/agent-tools#%E4%B8%80%E4%B8%AA%E5%B7%A5%E5%85%B7%E7%94%B1%E4%BB%80%E4%B9%88%E7%BB%84%E6%88%90" class="hash-link" aria-label="一个工具由什么组成的直接链接" title="一个工具由什么组成的直接链接" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">要素</th><th style="text-align:center">作用</th><th style="text-align:center">给谁看</th></tr></thead><tbody><tr><td style="text-align:center"><code>name</code></td><td style="text-align:center">工具名称, 模型回传时用它指认</td><td style="text-align:center">模型, 代码</td></tr><tr><td style="text-align:center"><code>description</code></td><td style="text-align:center">这个工具做什么, 什么时候该用</td><td style="text-align:center">模型</td></tr><tr><td style="text-align:center"><code>parameters</code></td><td style="text-align:center">参数的 JSON Schema</td><td style="text-align:center">模型</td></tr><tr><td style="text-align:center">函数本身</td><td style="text-align:center">真正执行的 Python 代码</td><td style="text-align:center">代码</td></tr></tbody></table>
<p>前三项是发给模型的说明书, 第四项是本地实现. 模型看不到你的代码, 只看到前三项.</p>
<p><code>description</code> 本质上是提示词, 它决定模型选不选这个工具, 以及传什么样的参数. 同一段代码, 说明写成"执行命令"和写成"在当前工作目录执行 shell 命令, 返回退出码和输出", 模型的表现不一样.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="装饰器与注册表">装饰器与注册表<a href="https://ranjian.pages.dev/blog/agent-tools#%E8%A3%85%E9%A5%B0%E5%99%A8%E4%B8%8E%E6%B3%A8%E5%86%8C%E8%A1%A8" class="hash-link" aria-label="装饰器与注册表的直接链接" title="装饰器与注册表的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="tool-做了什么"><code>@tool</code> 做了什么<a href="https://ranjian.pages.dev/blog/agent-tools#tool-%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88" class="hash-link" aria-label="tool-做了什么的直接链接" title="tool-做了什么的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">tool</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> description</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> parameters</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token triple-quoted-string string" style="color:rgb(195, 232, 141)">"""创建用于注册模型工具说明的装饰器."""</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">decorator</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">func</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        TOOLS</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            </span><span class="token string" style="color:rgb(195, 232, 141)">"type"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"function"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            </span><span class="token string" style="color:rgb(195, 232, 141)">"function"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                </span><span class="token string" style="color:rgb(195, 232, 141)">"name"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                </span><span class="token string" style="color:rgb(195, 232, 141)">"description"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> description</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                </span><span class="token string" style="color:rgb(195, 232, 141)">"parameters"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                    </span><span class="token string" style="color:rgb(195, 232, 141)">"type"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"object"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                    </span><span class="token string" style="color:rgb(195, 232, 141)">"properties"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain">k</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string" style="color:rgb(195, 232, 141)">"type"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"string"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"description"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> v</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">for</span><span class="token plain"> k</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> v </span><span class="token keyword" style="font-style:italic">in</span><span class="token plain"> parameters</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">items</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                    </span><span class="token string" style="color:rgb(195, 232, 141)">"required"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">list</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">parameters</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">keys</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">                </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">            </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        TOOL_NAME</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> func</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> func</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> decorator</span><br></div></code></pre></div></div>
<p>分成两层, 执行顺序是:</p>
<ol>
<li class=""><code>tool("list_dir", "...", {...})</code> 先执行, 把名称, 描述和参数记在闭包里, 返回 <code>decorator</code></li>
<li class=""><code>@</code> 把下面定义好的函数交给 <code>decorator</code></li>
<li class=""><code>decorator</code> 往 <code>TOOLS</code> 追加一条说明, 往 <code>TOOL_NAME</code> 登记函数, 然后把原函数原样返回</li>
</ol>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token decorator annotation punctuation" style="color:rgb(199, 146, 234)">@tool</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">name</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token string" style="color:rgb(195, 232, 141)">"read_file"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> description</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token string" style="color:rgb(195, 232, 141)">"读取本地文件内容"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> parameters</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string" style="color:rgb(195, 232, 141)">"path"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"文件位置"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">read_file</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">path</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">-</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><br></div></code></pre></div></div>
<p>装饰之后 <code>read_file</code> 还是原来那个函数, 可以直接调用, 也可以按名字查出来调用.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="两份数据">两份数据<a href="https://ranjian.pages.dev/blog/agent-tools#%E4%B8%A4%E4%BB%BD%E6%95%B0%E6%8D%AE" class="hash-link" aria-label="两份数据的直接链接" title="两份数据的直接链接" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:center">变量</th><th style="text-align:center">内容</th><th style="text-align:center">给谁用</th></tr></thead><tbody><tr><td style="text-align:center"><code>TOOLS</code></td><td style="text-align:center">列表, 每条是模型的工具说明</td><td style="text-align:center">每轮请求的 <code>tools</code> 参数</td></tr><tr><td style="text-align:center"><code>TOOL_NAME</code></td><td style="text-align:center">字典, 工具名到函数的映射</td><td style="text-align:center">执行工具时按名字找函数</td></tr></tbody></table>
<p>第 1 课也有这两份数据, 叫 <code>TOOLS</code> 和 <code>TOOL_MAP</code>. 区别只在名字和填空时机: 第 1 课是手写列表, 本课是装饰器在导入时自动追加.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="注册发生在什么时候">注册发生在什么时候<a href="https://ranjian.pages.dev/blog/agent-tools#%E6%B3%A8%E5%86%8C%E5%8F%91%E7%94%9F%E5%9C%A8%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99" class="hash-link" aria-label="注册发生在什么时候的直接链接" title="注册发生在什么时候的直接链接" translate="no">​</a></h3>
<p><code>@tool(...)</code> 在 <strong>模块导入时</strong> 就执行完, 不是调用工具时. 所以脚本一启动, <code>TOOLS</code> 里已经有 7 条说明, <code>TOOL_NAME</code> 里已经有 7 个函数.</p>
<p>顺序就是代码里函数定义的顺序: <code>list_dir</code>, <code>write_file</code>, <code>run_shell</code>, <code>fetch_url</code>, <code>read_file</code>, <code>get_weather</code>, <code>get_current_time</code>. 这个顺序只是说明书的排列顺序, 不影响模型选谁.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="类型标注保住了什么">类型标注保住了什么<a href="https://ranjian.pages.dev/blog/agent-tools#%E7%B1%BB%E5%9E%8B%E6%A0%87%E6%B3%A8%E4%BF%9D%E4%BD%8F%E4%BA%86%E4%BB%80%E4%B9%88" class="hash-link" aria-label="类型标注保住了什么的直接链接" title="类型标注保住了什么的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">ToolFunction </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> TypeVar</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">"ToolFunction"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> bound</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain">Callable</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">object</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">tool</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> description</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> parameters</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">dict</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">-</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token plain"> Callable</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain">ToolFunction</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> ToolFunction</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><br></div></code></pre></div></div>
<p>装饰器如果不标注, 类型检查器会认为被装饰的函数变成了"任意可调用对象", 函数名和参数都对不上. 用 <code>TypeVar</code> 绑定后, 装饰前后签名一致, 编辑器还能提示 <code>read_file(path=...)</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="参数简写的代价">参数简写的代价<a href="https://ranjian.pages.dev/blog/agent-tools#%E5%8F%82%E6%95%B0%E7%AE%80%E5%86%99%E7%9A%84%E4%BB%A3%E4%BB%B7" class="hash-link" aria-label="参数简写的代价的直接链接" title="参数简写的代价的直接链接" translate="no">​</a></h3>
<p><code>parameters</code> 收的是"参数名到说明"的字典, 装饰器统一展开成:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token property">"path"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"type"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"string"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token property">"description"</span><span class="token operator" style="color:rgb(137, 221, 255)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"文件位置"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>这带来两个限制:</p>
<ul>
<li class="">所有参数都是 <code>string</code>, 写不了 <code>integer</code>, <code>enum</code>, 数组</li>
<li class="">所有参数都进 <code>required</code>, 写不了可选参数</li>
</ul>
<p>本课 7 个工具的参数刚好都是必填字符串, 所以够用. 遇到 <code>timeout</code> 这类有默认值的参数, 就得自己写完整 schema, 不能再用这个简写.</p>
<p>无参数工具传空字典, 展开后 <code>required</code> 是空列表:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token decorator annotation punctuation" style="color:rgb(199, 146, 234)">@tool</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">name</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token string" style="color:rgb(195, 232, 141)">"get_current_time"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> description</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token string" style="color:rgb(195, 232, 141)">"获取本机当前日期和时间（使用系统本地时区）"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> parameters</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="七个工具">七个工具<a href="https://ranjian.pages.dev/blog/agent-tools#%E4%B8%83%E4%B8%AA%E5%B7%A5%E5%85%B7" class="hash-link" aria-label="七个工具的直接链接" title="七个工具的直接链接" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">工具</th><th style="text-align:center">参数</th><th style="text-align:center">特殊处理</th></tr></thead><tbody><tr><td style="text-align:center"><code>list_dir</code></td><td style="text-align:center"><code>path</code></td><td style="text-align:center">目录名补 <code>/</code>, 结果截断</td></tr><tr><td style="text-align:center"><code>read_file</code></td><td style="text-align:center"><code>path</code></td><td style="text-align:center">不确认, 不截断</td></tr><tr><td style="text-align:center"><code>write_file</code></td><td style="text-align:center"><code>path</code>, <code>content</code></td><td style="text-align:center">执行前确认, 自动建父目录</td></tr><tr><td style="text-align:center"><code>run_shell</code></td><td style="text-align:center"><code>command</code></td><td style="text-align:center">执行前确认, 30 秒超时, 输出分两路</td></tr><tr><td style="text-align:center"><code>fetch_url</code></td><td style="text-align:center"><code>url</code></td><td style="text-align:center">校验协议, 按字节截断, 15 秒超时</td></tr><tr><td style="text-align:center"><code>get_weather</code></td><td style="text-align:center"><code>city</code></td><td style="text-align:center">返回字典, 固定模拟数据</td></tr><tr><td style="text-align:center"><code>get_current_time</code></td><td style="text-align:center">无</td><td style="text-align:center">无参数, 返回本地时间字符串</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="确认放在工具里">确认放在工具里<a href="https://ranjian.pages.dev/blog/agent-tools#%E7%A1%AE%E8%AE%A4%E6%94%BE%E5%9C%A8%E5%B7%A5%E5%85%B7%E9%87%8C" class="hash-link" aria-label="确认放在工具里的直接链接" title="确认放在工具里的直接链接" translate="no">​</a></h3>
<p><code>write_file</code> 和 <code>run_shell</code> 都先 <code>input()</code> 问一句 <code>y/n</code>:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">confirmation </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">input</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">f"\n[需要确认] 写入文件：</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation">path</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">（</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation builtin" style="color:rgb(130, 170, 255)">len</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string-interpolation interpolation">content</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)"> 字符，已有文件会被覆盖）\n"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token string" style="color:rgb(195, 232, 141)">"允许？(y/n) "</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain"></span><span class="token keyword" style="font-style:italic">if</span><span class="token plain"> confirmation</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">strip</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">lower</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">!=</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"y"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"用户拒绝写入文件"</span><br></div></code></pre></div></div>
<p>放在工具里而不是循环里, 原因是: 循环只认"工具名加参数", 它不知道哪个工具危险. 危险与否是工具自己的属性, 写在工具里才对.</p>
<p>用户拒绝时返回的字符串会作为 <code>tool</code> 消息进历史, 模型下一轮能看到"用户拒绝了", 不会死循环重试.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="截断分三种口径">截断分三种口径<a href="https://ranjian.pages.dev/blog/agent-tools#%E6%88%AA%E6%96%AD%E5%88%86%E4%B8%89%E7%A7%8D%E5%8F%A3%E5%BE%84" class="hash-link" aria-label="截断分三种口径的直接链接" title="截断分三种口径的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">truncate_output</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">text</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> limit</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">int</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> MAX_OUTPUT</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">-</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">if</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">len</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">text</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">&lt;=</span><span class="token plain"> limit</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> text</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> </span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">f"</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation">text</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token string-interpolation interpolation format-spec">limit]</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">\n...（已截断，原文共 </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation builtin" style="color:rgb(130, 170, 255)">len</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string-interpolation interpolation">text</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)"> 个字符）"</span><br></div></code></pre></div></div>
<ul>
<li class="">目录列表: 按字符数截断, 上限 8000</li>
<li class="">命令: 标准输出和标准错误各 4000, 合计不超过 8000</li>
<li class="">网页: 先只读 8001 字节, 超过 8000 就截断并说明</li>
<li class="">文件读取: 不截断, <code>read_file</code> 直接返回全文</li>
</ul>
<p>网页用字节而不是字符, 是因为此时还没解码, 不知道一个字符占几个字节.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="工具函数不吞异常">工具函数不吞异常<a href="https://ranjian.pages.dev/blog/agent-tools#%E5%B7%A5%E5%85%B7%E5%87%BD%E6%95%B0%E4%B8%8D%E5%90%9E%E5%BC%82%E5%B8%B8" class="hash-link" aria-label="工具函数不吞异常的直接链接" title="工具函数不吞异常的直接链接" translate="no">​</a></h3>
<p><code>fetch_url</code> 遇到不合法的地址直接抛:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">if</span><span class="token plain"> parsed</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">scheme </span><span class="token keyword" style="font-style:italic">not</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">in</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">"http"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"https"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">or</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">not</span><span class="token plain"> parsed</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">hostname</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">raise</span><span class="token plain"> ValueError</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">"只支持完整的 HTTP 或 HTTPS 地址"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>这不是疏忽. 工具只管干活, 出错就抛; 统一由下一节的执行器接住, 转成模型能读的文字. 这样每个工具里都不用复制一遍 <code>try/except</code>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="统一执行器-excute">统一执行器 <code>excute</code><a href="https://ranjian.pages.dev/blog/agent-tools#%E7%BB%9F%E4%B8%80%E6%89%A7%E8%A1%8C%E5%99%A8-excute" class="hash-link" aria-label="统一执行器-excute的直接链接" title="统一执行器-excute的直接链接" translate="no">​</a></h2>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">def</span><span class="token plain"> </span><span class="token function" style="color:rgb(130, 170, 255)">excute</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">tool_name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> tool_args</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">dict</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">-</span><span class="token operator" style="color:rgb(137, 221, 255)">&gt;</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">|</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">dict</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token plain"> </span><span class="token operator" style="color:rgb(137, 221, 255)">|</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">int</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">if</span><span class="token plain"> tool_name </span><span class="token keyword" style="font-style:italic">not</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">in</span><span class="token plain"> TOOL_NAME</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> </span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">f"工具 </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation">tool_name</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)"> 不存在"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    tool_func </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> TOOL_NAME</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token plain">tool_name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">try</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        res </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> tool_func</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token operator" style="color:rgb(137, 221, 255)">**</span><span class="token plain">tool_args</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> res</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">except</span><span class="token plain"> Exception </span><span class="token keyword" style="font-style:italic">as</span><span class="token plain"> e</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> </span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">f"调用工具 </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation">tool_name</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)"> 时发生错误: </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation builtin" style="color:rgb(130, 170, 255)">str</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string-interpolation interpolation">e</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token string-interpolation interpolation format-spec">4000]</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">"</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="错误也是一种结果">错误也是一种结果<a href="https://ranjian.pages.dev/blog/agent-tools#%E9%94%99%E8%AF%AF%E4%B9%9F%E6%98%AF%E4%B8%80%E7%A7%8D%E7%BB%93%E6%9E%9C" class="hash-link" aria-label="错误也是一种结果的直接链接" title="错误也是一种结果的直接链接" translate="no">​</a></h3>
<p>这是本课最关键的设计. 工具报错时, 程序不崩, 而是把报错原文交回模型:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">调用工具 read_file 时发生错误: [Errno 2] No such file or directory: 'notes.txt'</span><br></div></code></pre></div></div>
<p>模型看到这句, 通常会先调 <code>list_dir</code> 看看目录里有什么, 再读正确的文件. 这个"看报错, 自己改"的过程, 就是 Agent 和普通脚本的区别.</p>
<p>如果这里直接抛异常, 整个程序退出, 模型连补救的机会都没有.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="工具名不存在也走这条路">工具名不存在也走这条路<a href="https://ranjian.pages.dev/blog/agent-tools#%E5%B7%A5%E5%85%B7%E5%90%8D%E4%B8%8D%E5%AD%98%E5%9C%A8%E4%B9%9F%E8%B5%B0%E8%BF%99%E6%9D%A1%E8%B7%AF" class="hash-link" aria-label="工具名不存在也走这条路的直接链接" title="工具名不存在也走这条路的直接链接" translate="no">​</a></h3>
<p>模型偶尔会编一个不存在的工具名. <code>TOOL_NAME</code> 里查不到, 直接返回一句"工具 xxx 不存在", 不用抛异常.</p>
<blockquote>
<p>💡 这里的提示只写了工具名, 没有列出可用工具. 参考实现会补一句"可用工具: ...", 模型改了名字后更容易一次改对. 要加的话, 在返回值里拼上 <code>", ".join(TOOL_NAME)</code> 即可.</p>
</blockquote>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="返回值可能是字典">返回值可能是字典<a href="https://ranjian.pages.dev/blog/agent-tools#%E8%BF%94%E5%9B%9E%E5%80%BC%E5%8F%AF%E8%83%BD%E6%98%AF%E5%AD%97%E5%85%B8" class="hash-link" aria-label="返回值可能是字典的直接链接" title="返回值可能是字典的直接链接" translate="no">​</a></h3>
<p><code>get_weather</code> 返回的是字典, 不是字符串:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string" style="color:rgb(195, 232, 141)">"city"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> city</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"temperature"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">25</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"description"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"晴天"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><br></div></code></pre></div></div>
<p>而 <code>tool</code> 消息的 <code>content</code> 必须是字符串, 所以循环里回填前要转:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">messages</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string" style="color:rgb(195, 232, 141)">"role"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool_call_id"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token builtin" style="color:rgb(130, 170, 255)">id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"content"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">tool_res</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>不转的话, 下一次请求会因字段类型不对而失败.</p>
<blockquote>
<p>📌 函数名是 <code>excute</code>, 是 <code>execute</code> 的拼写笔误. 当前代码按这个名字调用, 本文也照原样记录. 改名要同时改定义和调用处.</p>
</blockquote>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="主循环-run_task">主循环 <code>run_task</code><a href="https://ranjian.pages.dev/blog/agent-tools#%E4%B8%BB%E5%BE%AA%E7%8E%AF-run_task" class="hash-link" aria-label="主循环-run_task的直接链接" title="主循环-run_task的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="与第-1-课逐块对照">与第 1 课逐块对照<a href="https://ranjian.pages.dev/blog/agent-tools#%E4%B8%8E%E7%AC%AC-1-%E8%AF%BE%E9%80%90%E5%9D%97%E5%AF%B9%E7%85%A7" class="hash-link" aria-label="与第 1 课逐块对照的直接链接" title="与第 1 课逐块对照的直接链接" translate="no">​</a></h3>
<table><thead><tr><th style="text-align:center">部分</th><th style="text-align:center">第 1 课</th><th style="text-align:center">第 2 课</th></tr></thead><tbody><tr><td style="text-align:center">工具来源</td><td style="text-align:center">三个全局函数</td><td style="text-align:center">装饰器自动注册</td></tr><tr><td style="text-align:center">工具说明</td><td style="text-align:center">手写 <code>TOOLS</code> 列表</td><td style="text-align:center"><code>@tool</code> 生成</td></tr><tr><td style="text-align:center">执行</td><td style="text-align:center">循环里 <code>TOOL_MAP[name](**args)</code></td><td style="text-align:center">循环里 <code>excute(name, args)</code></td></tr><tr><td style="text-align:center">错误处理</td><td style="text-align:center">循环里的 <code>try/except</code></td><td style="text-align:center">执行器内部</td></tr><tr><td style="text-align:center">轮数上限</td><td style="text-align:center"><code>MAX_STEPS = 20</code></td><td style="text-align:center"><code>MAX_STEPS = 10</code></td></tr><tr><td style="text-align:center">入口</td><td style="text-align:center">写死一个 <code>task</code></td><td style="text-align:center">写死一个 <code>run_task(...)</code> 调用</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="循环本体">循环本体<a href="https://ranjian.pages.dev/blog/agent-tools#%E5%BE%AA%E7%8E%AF%E6%9C%AC%E4%BD%93" class="hash-link" aria-label="循环本体的直接链接" title="循环本体的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">for</span><span class="token plain"> step </span><span class="token keyword" style="font-style:italic">in</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">range</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token number" style="color:rgb(247, 140, 108)">1</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> MAX_STEPS </span><span class="token operator" style="color:rgb(137, 221, 255)">+</span><span class="token plain"> </span><span class="token number" style="color:rgb(247, 140, 108)">1</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    res </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> client</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">chat</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">completions</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">create</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">model</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain">MODEL</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> messages</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain">messages</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> tools</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain">TOOLS</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    message </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> res</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">choices</span><span class="token punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token number" style="color:rgb(247, 140, 108)">0</span><span class="token punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">message</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    messages</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">message</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">model_dump</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">exclude_none</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token boolean" style="color:rgb(255, 88, 116)">True</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    tool_calls </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> message</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">tool_calls</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">if</span><span class="token plain"> </span><span class="token keyword" style="font-style:italic">not</span><span class="token plain"> tool_calls</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">f'第 </span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation">step</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)"> 步模型没有调用工具，任务完成，输出结果：</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string-interpolation interpolation">res</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token string-interpolation interpolation">choices</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">[</span><span class="token string-interpolation interpolation number" style="color:rgb(247, 140, 108)">0</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">]</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token string-interpolation interpolation">message</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token string-interpolation interpolation">content</span><span class="token string-interpolation interpolation punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token string-interpolation string" style="color:rgb(195, 232, 141)">'</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        </span><span class="token keyword" style="font-style:italic">return</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    </span><span class="token keyword" style="font-style:italic">for</span><span class="token plain"> call </span><span class="token keyword" style="font-style:italic">in</span><span class="token plain"> tool_calls</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        tool_name </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">function</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">name</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        tool_args </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> json</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">loads</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">function</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">arguments</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        tool_res </span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token plain"> excute</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">tool_name</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> tool_args</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">        messages</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">append</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token punctuation" style="color:rgb(199, 146, 234)">{</span><span class="token string" style="color:rgb(195, 232, 141)">"role"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"tool_call_id"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> call</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token builtin" style="color:rgb(130, 170, 255)">id</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(195, 232, 141)">"content"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(130, 170, 255)">str</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">tool_res</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">}</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>和第 1 课相比, 循环里已经没有任何具体工具的名字. 它只做四件事: 问模型, 记助手消息, 分发工具调用, 回填结果.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="三处没变的地方">三处没变的地方<a href="https://ranjian.pages.dev/blog/agent-tools#%E4%B8%89%E5%A4%84%E6%B2%A1%E5%8F%98%E7%9A%84%E5%9C%B0%E6%96%B9" class="hash-link" aria-label="三处没变的地方的直接链接" title="三处没变的地方的直接链接" translate="no">​</a></h3>
<ul>
<li class="">先追加助手消息, 再处理工具结果. 顺序反了下一次请求会报缺少对应调用</li>
<li class=""><code>tool_call_id</code> 用 <code>call.id</code>, 不是工具名</li>
<li class="">一轮可能有多个 <code>tool_calls</code>, 每个都要回一条 <code>tool</code> 消息</li>
</ul>
<p>这三条是协议要求, 不因工具变多而变.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="注释掉的那行">注释掉的那行<a href="https://ranjian.pages.dev/blog/agent-tools#%E6%B3%A8%E9%87%8A%E6%8E%89%E7%9A%84%E9%82%A3%E8%A1%8C" class="hash-link" aria-label="注释掉的那行的直接链接" title="注释掉的那行的直接链接" translate="no">​</a></h3>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token comment" style="color:rgb(105, 112, 152);font-style:italic"># print(f'第 {step} 步模型工具调用请求：{tool_calls}\n')</span><br></div></code></pre></div></div>
<p>调试时用过的打印, 已经注释掉. 想看原始结构时取消注释即可.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="运行与边界">运行与边界<a href="https://ranjian.pages.dev/blog/agent-tools#%E8%BF%90%E8%A1%8C%E4%B8%8E%E8%BE%B9%E7%95%8C" class="hash-link" aria-label="运行与边界的直接链接" title="运行与边界的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="跑起来">跑起来<a href="https://ranjian.pages.dev/blog/agent-tools#%E8%B7%91%E8%B5%B7%E6%9D%A5" class="hash-link" aria-label="跑起来的直接链接" title="跑起来的直接链接" translate="no">​</a></h3>
<p>当前入口写死了一个任务:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">run_task</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">"请列出当前目录，创建 notes/hello.txt 写入 Hello, Agent!，然后读取并总结。"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>在 <code>playground</code> 目录里跑, 预期过程:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">=== 第 1 步 ===</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">调用工具名称:list_dir,参数:{'path': '.'}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">=== 第 2 步 ===</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">调用工具名称:write_file,参数:{'path': 'notes/hello.txt', 'content': 'Hello, Agent!'}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">[需要确认] 写入文件：notes/hello.txt（13 字符，已有文件会被覆盖）</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">允许？(y/n) y</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">=== 第 3 步 ===</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">调用工具名称:read_file,参数:{'path': 'notes/hello.txt'}</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">=== 第 4 步 ===</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">第 4 步模型没有调用工具，任务完成，输出结果：已创建 notes/hello.txt，内容为 Hello, Agent!</span><br></div></code></pre></div></div>
<p>模型可能少走一步, 多走一步, 或者把读取和创建的顺序换掉. 这都正常, 只要最后的结果对.</p>
<p>三个观察点:</p>
<ul>
<li class="">每步打印的工具名和参数, 是不是你预期的路径</li>
<li class="">写文件和执行命令时, 确认提示出现了没有</li>
<li class="">最后一条 <code>assistant</code> 消息里, <code>tool_calls</code> 是不是 <code>None</code></li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="这一课的边界">这一课的边界<a href="https://ranjian.pages.dev/blog/agent-tools#%E8%BF%99%E4%B8%80%E8%AF%BE%E7%9A%84%E8%BE%B9%E7%95%8C" class="hash-link" aria-label="这一课的边界的直接链接" title="这一课的边界的直接链接" translate="no">​</a></h3>
<ul>
<li class=""><code>read_file</code>, <code>list_dir</code>, <code>fetch_url</code> 不确认, 模型可以随便读, 随便访问任意 http 地址</li>
<li class="">相对路径以 Python 进程的当前工作目录为准, 在 <code>code</code> 目录里跑会把文件写进 <code>code</code></li>
<li class="">停止条件仍是"模型不再请求工具", 不代表任务验证成功</li>
<li class=""><code>MAX_STEPS</code> 是 10, 一轮里有多个工具也只占一轮</li>
<li class="">模型请求本身失败 (网络, 鉴权) 不被 <code>excute</code> 捕获, 会直接中断</li>
</ul>
<p>最后一条尤其要注意: 第 1 课循环里的 <code>try/except</code> 也是只包工具, 不包 <code>create()</code>. 本课把工具的异常搬到执行器里, 这个边界没有变.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="与参考实现的差异">与参考实现的差异<a href="https://ranjian.pages.dev/blog/agent-tools#%E4%B8%8E%E5%8F%82%E8%80%83%E5%AE%9E%E7%8E%B0%E7%9A%84%E5%B7%AE%E5%BC%82" class="hash-link" aria-label="与参考实现的差异的直接链接" title="与参考实现的差异的直接链接" translate="no">​</a></h2>
<table><thead><tr><th style="text-align:center">方面</th><th style="text-align:center">自己的实现</th><th style="text-align:center">NamiAgent 第 2 课</th></tr></thead><tbody><tr><td style="text-align:center">文件拆分</td><td style="text-align:center">单文件 <code>tools.py</code></td><td style="text-align:center"><code>tools.py</code> 加 <code>agent.py</code></td></tr><tr><td style="text-align:center">注册结构</td><td style="text-align:center"><code>TOOLS</code> 列表, <code>TOOL_NAME</code> 字典</td><td style="text-align:center"><code>TOOLS</code> 字典, 值里同时存说明和函数</td></tr><tr><td style="text-align:center">参数简写</td><td style="text-align:center">在装饰器里展开</td><td style="text-align:center">单独的 <code>_params()</code> 函数</td></tr><tr><td style="text-align:center">写文件确认</td><td style="text-align:center">有</td><td style="text-align:center">无</td></tr><tr><td style="text-align:center">执行器名</td><td style="text-align:center"><code>excute</code></td><td style="text-align:center"><code>execute</code></td></tr><tr><td style="text-align:center">返回值处理</td><td style="text-align:center">执行器返回原值, 循环里 <code>str()</code></td><td style="text-align:center">工具函数内 <code>str()</code></td></tr><tr><td style="text-align:center">主循环入口</td><td style="text-align:center">写死一个任务</td><td style="text-align:center">交互式 <code>input()</code> 多轮对话</td></tr><tr><td style="text-align:center">工具数量</td><td style="text-align:center">7 个</td><td style="text-align:center">5 个</td></tr></tbody></table>
<p>剩下的部分一致: 装饰器注册, 说明与函数同步, 错误当结果, 输出截断, 轮数上限.</p>
<p>参考实现把 <code>messages</code> 提到循环外, 支持连续多轮对话; 本课每次 <code>run_task()</code> 都从新历史开始. 下一课改权限时再决定要不要保留会话.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="断点调试">断点调试<a href="https://ranjian.pages.dev/blog/agent-tools#%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95" class="hash-link" aria-label="断点调试的直接链接" title="断点调试的直接链接" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="看注册结果">看注册结果<a href="https://ranjian.pages.dev/blog/agent-tools#%E7%9C%8B%E6%B3%A8%E5%86%8C%E7%BB%93%E6%9E%9C" class="hash-link" aria-label="看注册结果的直接链接" title="看注册结果的直接链接" translate="no">​</a></h3>
<p>在第一个 <code>@tool(...)</code> 之后任意位置打断点, 或在文件末尾加一行:</p>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">json</span><span class="token punctuation" style="color:rgb(199, 146, 234)">.</span><span class="token plain">dumps</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token plain">TOOLS</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> indent</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token number" style="color:rgb(247, 140, 108)">2</span><span class="token punctuation" style="color:rgb(199, 146, 234)">,</span><span class="token plain"> ensure_ascii</span><span class="token operator" style="color:rgb(137, 221, 255)">=</span><span class="token boolean" style="color:rgb(255, 88, 116)">False</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>能直接检查七条说明的 <code>name</code> 和 <code>properties</code> 有没有写错. 这个打印只用于调试, 不要留在循环里.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="看一次工具调用">看一次工具调用<a href="https://ranjian.pages.dev/blog/agent-tools#%E7%9C%8B%E4%B8%80%E6%AC%A1%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8" class="hash-link" aria-label="看一次工具调用的直接链接" title="看一次工具调用的直接链接" translate="no">​</a></h3>
<p>在 <code>tool_args = json.loads(call.function.arguments)</code> 下一行打断点, 展开:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">call</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  id</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">  function</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    name</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    arguments   ← 字符串</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">tool_args       ← 解析后的字典</span><br></div></code></pre></div></div>
<p>重点看 <code>tool_args</code> 的键能不能对上函数参数名, 对不上会在 <code>tool_func(**tool_args)</code> 处抛 <code>TypeError</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="看回填的消息">看回填的消息<a href="https://ranjian.pages.dev/blog/agent-tools#%E7%9C%8B%E5%9B%9E%E5%A1%AB%E7%9A%84%E6%B6%88%E6%81%AF" class="hash-link" aria-label="看回填的消息的直接链接" title="看回填的消息的直接链接" translate="no">​</a></h3>
<p>在 <code>messages.append({"role": "tool", ...})</code> 下一行打断点, 检查:</p>
<ul>
<li class=""><code>content</code> 是字符串, 不是字典</li>
<li class=""><code>tool_call_id</code> 和刚才 <code>call.id</code> 一致</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="参考资料">参考资料<a href="https://ranjian.pages.dev/blog/agent-tools#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" class="hash-link" aria-label="参考资料的直接链接" title="参考资料的直接链接" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://api-docs.deepseek.com/api/create-chat-completion/" target="_blank" rel="noopener noreferrer" class="">DeepSeek Chat Completions API</a>: <code>tools</code> 参数和 <code>tool_calls</code> 返回字段.</li>
<li class=""><a href="https://developers.openai.com/api/docs/guides/function-calling" target="_blank" rel="noopener noreferrer" class="">OpenAI Function Calling 指南</a>: 工具说明的结构与调用流程.</li>
<li class=""><a href="https://json-schema.org/understanding-json-schema/" target="_blank" rel="noopener noreferrer" class="">JSON Schema 入门</a>: <code>parameters</code> 里各关键字的含义.</li>
<li class="">参考实现: NamiAgent 的 <code>lesson-02/tools.py</code></li>
</ul>
<p><em>更新记录: 2026-11-02 创建, 按 <code>code/02-tools/tools.py</code> 当前实现整理, 补充装饰器注册, 七个工具, 执行器, 主循环对照与调试位置; 示例输出为示意, 不代表本次真实运行记录.</em></p>]]></content>
        <category label="Agent" term="Agent"/>
        <category label="Python" term="Python"/>
        <category label="工具调用" term="工具调用"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[染笺的小站是怎么建起来的]]></title>
        <id>https://ranjian.pages.dev/blog/blog-development</id>
        <link href="https://ranjian.pages.dev/blog/blog-development"/>
        <updated>2026-11-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[从参考博客、确定双主题，到本地文章发布助手与部署，记录小站的开发过程。]]></summary>
        <content type="html"><![CDATA[<p>这个小站从一个简单的想法开始：想拥有一个界面干净、可以长期记录学习过程的技术博客。文章在本地用 Markdown 写好，挑选并整理后，再发布到网站。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="从喜欢的博客出发">从喜欢的博客出发<a href="https://ranjian.pages.dev/blog/blog-development#%E4%BB%8E%E5%96%9C%E6%AC%A2%E7%9A%84%E5%8D%9A%E5%AE%A2%E5%87%BA%E5%8F%91" class="hash-link" aria-label="从喜欢的博客出发的直接链接" title="从喜欢的博客出发的直接链接" translate="no">​</a></h2>
<p>动手之前，我先看了其他人已经搭建好的博客。其中，<a href="https://blog.ysboke.cn/posts/" target="_blank" rel="noopener noreferrer" class="">勾玉博客</a>的阅读体验给了我启发：导航清楚，文章列表以文字为主，页面没有堆叠太多装饰。</p>
<p>我希望自己的博客也有类似的简洁感，同时保留一点温暖的颜色。讨论后，确定了两种可以切换的主题：清爽的纯白色，以及偏米白、带陶土色点缀的暖色。</p>
<p>正式实现前，我们制作了双主题概念图，用来比较配色：</p>
<p><img decoding="async" loading="lazy" alt="博客纯白主题与暖色主题的概念图" src="https://ranjian.pages.dev/assets/images/theme-concept-ccf29354c9460232b16fc9accd4e3f62.png" width="1942" height="809" class="img_ev3q"></p>
<p>这张图是设计参考，实际页面会根据阅读体验继续调整。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="确定页面和功能">确定页面和功能<a href="https://ranjian.pages.dev/blog/blog-development#%E7%A1%AE%E5%AE%9A%E9%A1%B5%E9%9D%A2%E5%92%8C%E5%8A%9F%E8%83%BD" class="hash-link" aria-label="确定页面和功能的直接链接" title="确定页面和功能的直接链接" translate="no">​</a></h2>
<p>首页采用“简短介绍 + 最新文章”的结构。读者打开网站后，既能知道这是谁的博客，也能直接看到最近发布的内容。</p>
<p>导航暂时分为五个入口：</p>
<ul>
<li class=""><strong>博客</strong>：查看文章列表和文章详情。</li>
<li class=""><strong>分类</strong>：按主题查找文章，最多支持两级分类。</li>
<li class=""><strong>归档</strong>：按时间浏览发布过的文章。</li>
<li class=""><strong>我的项目</strong>：以后整理项目介绍和实践经历。</li>
<li class=""><strong>关于我</strong>：以后补充个人介绍和联系方式。</li>
</ul>
<p>一篇文章可以有一个主要分类和多个标签。搜索覆盖标题、分类、标签和正文，并优先展示标题匹配的结果。文章页暂时不提供评论功能。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="选择技术方案">选择技术方案<a href="https://ranjian.pages.dev/blog/blog-development#%E9%80%89%E6%8B%A9%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88" class="hash-link" aria-label="选择技术方案的直接链接" title="选择技术方案的直接链接" translate="no">​</a></h2>
<p>平时已经习惯用 Markdown 写学习笔记，所以这个博客选择了静态网站的方式：用 Docusaurus 把文章和页面生成为网站，再通过 GitHub 和 Cloudflare Pages 完成部署。</p>
<p>发布流程大致是：</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token plain">本地 Markdown</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    ↓</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">挑选并整理为博客文章</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    ↓</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">提交到 GitHub 私有仓库</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    ↓</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">Cloudflare Pages 自动构建</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">    ↓</span><br></div><div class="token-line" style="color:#bfc7d5"><span class="token plain">访问线上博客</span><br></div></code></pre></div></div>
<p>GitHub 仓库用于保存源码和文章；Cloudflare Pages 负责根据仓库里的内容构建和发布网站。仓库是私有的，但发布后的网站可以公开访问。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="做一个本地发布助手">做一个本地发布助手<a href="https://ranjian.pages.dev/blog/blog-development#%E5%81%9A%E4%B8%80%E4%B8%AA%E6%9C%AC%E5%9C%B0%E5%8F%91%E5%B8%83%E5%8A%A9%E6%89%8B" class="hash-link" aria-label="做一个本地发布助手的直接链接" title="做一个本地发布助手的直接链接" translate="no">​</a></h2>
<p>如果每篇文章都手动整理元信息、复制图片、执行 Git 命令，用久了可能比较繁琐。所以这个项目还做了一个在本机运行的文章发布助手。</p>
<p>助手提供图形界面，用来选择 Markdown 文件、填写标题和分类、添加标签，以及查看发布预览。对于支持的本地图片，它会生成博客用的副本并调整路径，保留原来的学习笔记。</p>
<p>确认发布后，助手会提交本次选择的文章和相关图片，再推送到博客仓库。Git 推送和网站部署是两个不同的步骤：推送成功后，还需要等 Cloudflare 完成构建，网站才会更新。</p>
<p>目前助手主要支持标准 Markdown 图片写法；引用式图片和 HTML 图片尚不支持。真实学习文章的完整发布流程还需要继续验证，已发布文章的图形化更新流程也有待完善。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="上线后遇到的问题">上线后遇到的问题<a href="https://ranjian.pages.dev/blog/blog-development#%E4%B8%8A%E7%BA%BF%E5%90%8E%E9%81%87%E5%88%B0%E7%9A%84%E9%97%AE%E9%A2%98" class="hash-link" aria-label="上线后遇到的问题的直接链接" title="上线后遇到的问题的直接链接" translate="no">​</a></h2>
<p>博客上线后，分类页面出现过崩溃。最开始根据报错做了一次空值保护，但问题仍然存在。后来结合浏览器控制台提供的调用堆栈，才定位到生产构建中的实际问题：分类数据使用的 Map 在展开时，被构建工具转换成了不合适的数组操作。</p>
<p>修复时，将遍历方式改为显式使用 <code>Array.from(...)</code>，再重新构建并发布。这次排查也提醒我：<strong>构建通过和部署成功，并不等于页面在浏览器里一定能正常使用。</strong> 后续还需要把实际页面交互验证做得更完整。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="接下来">接下来<a href="https://ranjian.pages.dev/blog/blog-development#%E6%8E%A5%E4%B8%8B%E6%9D%A5" class="hash-link" aria-label="接下来的直接链接" title="接下来的直接链接" translate="no">​</a></h2>
<p>目前网站和发布助手已经有了第一版。后面准备逐步完成：</p>
<ul>
<li class="">整理并发布真正想分享的学习笔记。</li>
<li class="">补充“我的项目”和“关于我”页面。</li>
<li class="">继续检查手机端与不同浏览器的使用体验。</li>
<li class="">完善已发布文章的修改和更新流程。</li>
</ul>
<p>这个小站会随着学习和实践继续调整。先把内容写下来，再慢慢把体验做好。</p>]]></content>
        <category label="博客" term="博客"/>
        <category label="Docusaurus" term="Docusaurus"/>
        <category label="Cloudflare" term="Cloudflare"/>
        <category label="开发记录" term="开发记录"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[一起开始记录]]></title>
        <id>https://ranjian.pages.dev/blog/welcome</id>
        <link href="https://ranjian.pages.dev/blog/welcome"/>
        <updated>2026-11-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[这是一篇示例文章，用来检查博客的文章页面、分类、标签和图片。]]></summary>
        <content type="html"><![CDATA[<p>这是一篇示例文章。正式使用时，可以通过发布助手将自己的学习笔记整理到博客中。</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="一点记录">一点记录<a href="https://ranjian.pages.dev/blog/welcome#%E4%B8%80%E7%82%B9%E8%AE%B0%E5%BD%95" class="hash-link" aria-label="一点记录的直接链接" title="一点记录的直接链接" translate="no">​</a></h2>
<p>学习过程中留下的笔记、遇到的问题和解决方法，都可以慢慢整理成文章。</p>
<p><img decoding="async" loading="lazy" alt="一本打开的笔记本" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NDAgMjAwIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUiPjx0aXRsZSBpZD0idGl0bGUiPuS4gOacrOaJk+W8gOeahOeslOiusOacrDwvdGl0bGU+PHJlY3Qgd2lkdGg9IjY0MCIgaGVpZ2h0PSIyMDAiIHJ4PSIxNiIgZmlsbD0iI2Y0ZjBlOCIvPjxwYXRoIGQ9Ik0yMTAgNTNjNDUtMTMgNzgtOCAxMTAgMTAgMzItMTggNjUtMjMgMTEwLTEwdjkzYy00NS0xMy03OC04LTExMCAxMC0zMi0xOC02NS0yMy0xMTAtMTB6IiBmaWxsPSIjZmZmZGY4IiBzdHJva2U9IiNhNjZkNTUiIHN0cm9rZS13aWR0aD0iMyIvPjxwYXRoIGQ9Ik0zMjAgNjN2OTNNMjMyIDc4aDYwTTIzMiA5OGg2ME0zNDggNzhoNjBNMzQ4IDk4aDYwIiBmaWxsPSJub25lIiBzdHJva2U9IiNjNWFlOWMiIHN0cm9rZS13aWR0aD0iMyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+PC9zdmc+Cg==" width="640" height="200" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="一段示例代码">一段示例代码<a href="https://ranjian.pages.dev/blog/welcome#%E4%B8%80%E6%AE%B5%E7%A4%BA%E4%BE%8B%E4%BB%A3%E7%A0%81" class="hash-link" aria-label="一段示例代码的直接链接" title="一段示例代码的直接链接" translate="no">​</a></h2>
<div class="language-python codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#bfc7d5;--prism-background-color:#292d3e"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-python codeBlock_bY9V thin-scrollbar" style="color:#bfc7d5;background-color:#292d3e"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#bfc7d5"><span class="token keyword" style="font-style:italic">print</span><span class="token punctuation" style="color:rgb(199, 146, 234)">(</span><span class="token string" style="color:rgb(195, 232, 141)">"记录学习，分享实践"</span><span class="token punctuation" style="color:rgb(199, 146, 234)">)</span><br></div></code></pre></div></div>
<p>以上内容是示例，可以在准备好自己的文章后替换。</p>]]></content>
        <category label="博客" term="博客"/>
        <category label="示例" term="示例"/>
    </entry>
</feed>