<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: goodlords</title>
    <description>The latest articles on DEV Community by goodlords (@zealot2002).</description>
    <link>https://dev.to/zealot2002</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3939247%2F9f37a317-a504-4090-9ddd-7305f6de5be2.jpeg</url>
      <title>DEV Community: goodlords</title>
      <link>https://dev.to/zealot2002</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zealot2002"/>
    <language>en</language>
    <item>
      <title>I built a local-first membership app for one-chair hair salons</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Fri, 06 Nov 2026 06:37:03 +0000</pubDate>
      <link>https://dev.to/zealot2002/i-built-a-local-first-membership-app-for-one-chair-hair-salons-1poi</link>
      <guid>https://dev.to/zealot2002/i-built-a-local-first-membership-app-for-one-chair-hair-salons-1poi</guid>
      <description>&lt;p&gt;Small salons do not need a heavy SaaS checkout stack.&lt;/p&gt;

&lt;p&gt;They need three things that actually stick:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find a customer by name in seconds&lt;/li&gt;
&lt;li&gt;Deduct a visit or top up a card right after the cut&lt;/li&gt;
&lt;li&gt;See tonight’s real cash vs bonus credit in one place&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notebooks break when two people write on the same page. WeChat / Alipay history proves money moved — it does &lt;strong&gt;not&lt;/strong&gt; tell you whose card still has balance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet Lao Lv Salon Member
&lt;/h2&gt;

&lt;p&gt;I shipped a tiny Android app for that job.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data stays on the phone (no account signup)&lt;/li&gt;
&lt;li&gt;Search → balance → deduct / recharge&lt;/li&gt;
&lt;li&gt;“Today’s business” summary for closing the shop&lt;/li&gt;
&lt;li&gt;Works offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a short Chinese-subtitled walkthrough (about 17 seconds):&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Landing + free trial download:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://hairsalon-ayn.pages.dev/" rel="noopener noreferrer"&gt;https://hairsalon-ayn.pages.dev/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;APK installs without an account. Use it for a while; if it fits the chair, keep going. Pricing for a continued license is listed inside the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is (and is not) for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt; one-chair / couple-run shops that want a simple on-device ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not a fit:&lt;/strong&gt; multi-store chains that need HQ reports and shared cloud inventory — pick a real salon SaaS for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;If you run (or build for) tiny local service shops, I would love notes on what still hurts after a week of use — search, deduct flow, backup, or closing summary.&lt;/p&gt;

&lt;p&gt;Thanks for watching the clip.&lt;/p&gt;

</description>
      <category>productivity</category>
    </item>
    <item>
      <title>从零手写 Agent（七）：和商用 Agent 还差什么——个人调研 Agent 的边界</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:08:37 +0000</pubDate>
      <link>https://dev.to/zealot2002/cong-ling-shou-xie-agentqi-he-shang-yong-agent-huan-chai-shi-yao-ge-ren-diao-yan-agent-de-bian-jie-53h9</link>
      <guid>https://dev.to/zealot2002/cong-ling-shou-xie-agentqi-he-shang-yong-agent-huan-chai-shi-yao-ge-ren-diao-yan-agent-de-bian-jie-53h9</guid>
      <description>&lt;p&gt;前面几篇已经把一个能实际运行的 Agent 搭起来了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;有 Tool Calling&lt;/li&gt;
&lt;li&gt;有 Agent Loop&lt;/li&gt;
&lt;li&gt;有上下文管理和裁剪&lt;/li&gt;
&lt;li&gt;能联网搜索和抓取网页&lt;/li&gt;
&lt;li&gt;能记录 sources&lt;/li&gt;
&lt;li&gt;能生成 Markdown 报告并落盘&lt;/li&gt;
&lt;li&gt;有本地 Web UI&lt;/li&gt;
&lt;li&gt;可以接本地 Ollama，也可以接 OpenAI 兼容的云端模型&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;作为一个个人调研 Agent，这套东西已经能完成基本任务。&lt;/p&gt;

&lt;p&gt;但如果拿它和成熟的商用 Agent 放在一起比较，差距还是比较明显的。&lt;/p&gt;

&lt;h3&gt;
  
  
  1. 数据能力
&lt;/h3&gt;

&lt;p&gt;现在的 Agent 主要依赖：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;普通 Web 搜索&lt;/li&gt;
&lt;li&gt;HN / RSS&lt;/li&gt;
&lt;li&gt;网页抓取&lt;/li&gt;
&lt;li&gt;简单的 HTML 内容提取&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;商用 Agent 往往还需要处理：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF&lt;/li&gt;
&lt;li&gt;表格&lt;/li&gt;
&lt;li&gt;JavaScript 渲染页面&lt;/li&gt;
&lt;li&gt;私有知识库&lt;/li&gt;
&lt;li&gt;企业内部数据&lt;/li&gt;
&lt;li&gt;专业领域数据源&lt;/li&gt;
&lt;li&gt;付费搜索和数据 API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以第一层差距不是模型，而是&lt;strong&gt;数据获取和数据处理能力&lt;/strong&gt;。&lt;/p&gt;




&lt;h3&gt;
  
  
  2. 任务规划
&lt;/h3&gt;

&lt;p&gt;现在的 &lt;code&gt;_agent_loop&lt;/code&gt; 更接近：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;思考
→ 调 Tool
→ 看结果
→ 继续思考
→ 调 Tool
→ ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;对于“搜索几个资料，然后生成报告”这种任务已经够用。&lt;/p&gt;

&lt;p&gt;但任务变长以后，就会需要更明确的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan
→ Execute
→ Observe
→ Re-plan
→ Verify
→ Deliver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;例如一个复杂调研任务，可能需要拆成多个子任务，并记录哪些已经完成、哪些失败、哪些需要重新执行。&lt;/p&gt;

&lt;p&gt;当前版本没有专门的 Planner，也没有持久化的任务状态。&lt;/p&gt;




&lt;h3&gt;
  
  
  3. 结果可靠性和评估
&lt;/h3&gt;

&lt;p&gt;现在虽然已经记录了 &lt;code&gt;sources&lt;/code&gt;，但还没有真正建立完整的结果验证机制。&lt;/p&gt;

&lt;p&gt;例如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;搜索成功
≠
找到正确资料

找到资料
≠
引用正确

生成报告
≠
任务完成
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;当前版本最多可以知道 Agent 调用了哪些 Tool、拿到了哪些来源。&lt;/p&gt;

&lt;p&gt;如果要进一步提高可靠性，还需要：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;引用 URL 校验&lt;/li&gt;
&lt;li&gt;来源去重&lt;/li&gt;
&lt;li&gt;来源质量判断&lt;/li&gt;
&lt;li&gt;事实与来源对应关系检查&lt;/li&gt;
&lt;li&gt;自动评测&lt;/li&gt;
&lt;li&gt;回归测试&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;也就是说，需要从“能跑”进一步走向“可验证”。&lt;/p&gt;




&lt;h3&gt;
  
  
  4. 模型能力
&lt;/h3&gt;

&lt;p&gt;当前版本可以接本地 7B 模型，也可以切换到 OpenAI 兼容的云端模型。&lt;/p&gt;

&lt;p&gt;模型能力会直接影响：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool 选择&lt;/li&gt;
&lt;li&gt;参数生成&lt;/li&gt;
&lt;li&gt;多步任务执行&lt;/li&gt;
&lt;li&gt;上下文理解&lt;/li&gt;
&lt;li&gt;最终报告质量&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;特别是任务变复杂以后，小模型更容易出现：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;该调用 Tool 的时候不调用
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;或者：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;已经拿到结果，但不知道下一步应该做什么
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以 Agent 能力并不完全由 Loop 决定，&lt;strong&gt;模型本身也是整个系统的一部分&lt;/strong&gt;。&lt;/p&gt;




&lt;h3&gt;
  
  
  5. 工程和安全
&lt;/h3&gt;

&lt;p&gt;当前版本是本地个人项目，因此很多生产环境必须考虑的问题都没有做。&lt;/p&gt;

&lt;p&gt;例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户认证&lt;/li&gt;
&lt;li&gt;权限控制&lt;/li&gt;
&lt;li&gt;Rate Limit&lt;/li&gt;
&lt;li&gt;API Key 管理&lt;/li&gt;
&lt;li&gt;成本控制&lt;/li&gt;
&lt;li&gt;完整日志和审计&lt;/li&gt;
&lt;li&gt;多用户隔离&lt;/li&gt;
&lt;li&gt;Tool 权限控制&lt;/li&gt;
&lt;li&gt;SSRF 防护&lt;/li&gt;
&lt;li&gt;沙箱执行&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;尤其是加入 &lt;code&gt;fetch_url&lt;/code&gt;、Shell、代码执行这类 Tool 后，安全边界会迅速变复杂。&lt;/p&gt;

&lt;p&gt;本地 Demo 可以直接访问 URL。&lt;/p&gt;

&lt;p&gt;真正的线上 Agent 则不能简单地相信模型给出的 URL。&lt;/p&gt;




&lt;h3&gt;
  
  
  6. 长任务和状态持久化
&lt;/h3&gt;

&lt;p&gt;现在的 Agent 基本属于：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;一次请求
→ 一次执行
→ 一次结果
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;如果任务运行几十分钟甚至几个小时，就需要考虑：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;中途失败怎么办&lt;/li&gt;
&lt;li&gt;服务重启怎么办&lt;/li&gt;
&lt;li&gt;Tool 超时怎么办&lt;/li&gt;
&lt;li&gt;某一步执行失败要不要重试&lt;/li&gt;
&lt;li&gt;已经完成的步骤能不能恢复&lt;/li&gt;
&lt;li&gt;如何保存中间状态&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这时候 Agent 就不再只是一个函数调用，而更接近一个有状态的任务系统。&lt;/p&gt;




&lt;h2&gt;
  
  
  对照下来
&lt;/h2&gt;

&lt;p&gt;把目前这个 Agent 和商用 Agent 放在一起，差距大致可以归纳成：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th&gt;当前版本&lt;/th&gt;
&lt;th&gt;商用 Agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool Calling&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent Loop&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;上下文管理&lt;/td&gt;
&lt;td&gt;基础&lt;/td&gt;
&lt;td&gt;完整&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web 搜索&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;更丰富&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据源&lt;/td&gt;
&lt;td&gt;Web / HN / RSS&lt;/td&gt;
&lt;td&gt;Web / API / PDF / 私有数据等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;引用&lt;/td&gt;
&lt;td&gt;有 sources&lt;/td&gt;
&lt;td&gt;可验证、可评估&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;报告生成&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务规划&lt;/td&gt;
&lt;td&gt;基础&lt;/td&gt;
&lt;td&gt;多阶段规划&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长任务&lt;/td&gt;
&lt;td&gt;弱&lt;/td&gt;
&lt;td&gt;完整状态管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;失败恢复&lt;/td&gt;
&lt;td&gt;基础&lt;/td&gt;
&lt;td&gt;重试 / Checkpoint / Recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型&lt;/td&gt;
&lt;td&gt;本地 / 云端&lt;/td&gt;
&lt;td&gt;通常使用更强模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;评估&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;Eval / Regression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全&lt;/td&gt;
&lt;td&gt;基础&lt;/td&gt;
&lt;td&gt;权限 / 沙箱 / SSRF 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多用户&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;成本控制&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;审计&lt;/td&gt;
&lt;td&gt;基础日志&lt;/td&gt;
&lt;td&gt;完整 Trace / Audit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;从这个表看，差距其实比较清楚。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Loop 本身并没有那么复杂。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;真正拉开差距的，是 Loop 外面的这些东西：数据、状态、规划、验证、评估、安全，以及生产环境的工程能力。&lt;/p&gt;

&lt;p&gt;这也是目前这个项目最明确的边界：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;它已经是一个能工作的个人调研 Agent，但距离一个稳定、可验证、可运营的商用 Agent，还有一整层工程能力。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;到这里，这个系列的代码部分基本就完整了。&lt;/p&gt;

</description>
      <category>agents</category>
      <category>research</category>
      <category>ollama</category>
      <category>python</category>
    </item>
    <item>
      <title>从零手写 Agent（六）：云模型 API Key 与网页调研台</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:07:59 +0000</pubDate>
      <link>https://dev.to/zealot2002/cong-ling-shou-xie-agentliu-yun-mo-xing-api-key-yu-wang-ye-diao-yan-tai-loop-reng-bu-bian-10d0</link>
      <guid>https://dev.to/zealot2002/cong-ling-shou-xie-agentliu-yun-mo-xing-api-key-yu-wang-ye-diao-yan-tai-loop-reng-bu-bian-10d0</guid>
      <description>&lt;h1&gt;
  
  
  从零手写 Agent（六）：云模型 API Key 与网页调研台——Loop 仍不变
&lt;/h1&gt;

&lt;p&gt;第五篇把 &lt;strong&gt;CLI 调研 Agent&lt;/strong&gt; 跑通：搜、抓、sources、save。&lt;br&gt;&lt;br&gt;
接下来两件事很自然：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;换更强推理&lt;/strong&gt; — 本地 7B 写长报告仍吃力，用 &lt;strong&gt;OpenAI 兼容云端 API&lt;/strong&gt;（DeepSeek、OpenAI 等）只改配置，不改 Loop。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;把 trace 亮出来&lt;/strong&gt; — &lt;code&gt;-v --trace&lt;/code&gt; 对开发者够用；给「调研工具」做一个 &lt;strong&gt;网页台&lt;/strong&gt;，逐步看到 Step / Tool / 来源，比盯终端友好。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;第六篇就这两块：&lt;strong&gt;&lt;code&gt;LLM_*&lt;/code&gt; 环境变量 + 薄 FastAPI 壳&lt;/strong&gt;，核心仍是 &lt;code&gt;_agent_loop&lt;/code&gt;。&lt;/p&gt;


&lt;h2&gt;
  
  
  1. 云端 LLM：还是同一个 Chat Completions
&lt;/h2&gt;

&lt;p&gt;仓库从一开始就用 &lt;strong&gt;OpenAI Python SDK&lt;/strong&gt; 打 Ollama 的 &lt;code&gt;/v1&lt;/code&gt;，所以换云模型不是换框架，而是换 &lt;strong&gt;base_url + api_key + model 名&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.env&lt;/code&gt; 示例（见仓库 &lt;code&gt;.env.example&lt;/code&gt;）：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 本地默认
OLLAMA_BASE_URL=http://localhost:11434/v1
OLLAMA_MODEL=qwen2.5:7b

# 启用云端时（优先于 OLLAMA_*）
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_API_KEY=sk-……
LLM_MODEL=deepseek-chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;config.py&lt;/code&gt; 合并为：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM_BASE_URL  ← LLM_BASE_URL；或 LLM_PROVIDER=glm 时用智谱端点；否则 OLLAMA_BASE_URL
LLM_MODEL     ← LLM_MODEL；或 glm 默认 glm-4-flash；否则 OLLAMA_MODEL
LLM_API_KEY   ← LLM_API_KEY，或 shell 里的 GLM_API_KEY；都没有则占位 "ollama"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent/llm.py&lt;/code&gt; 的 &lt;code&gt;create_client()&lt;/code&gt; 始终 &lt;code&gt;OpenAI(base_url=..., api_key=...)&lt;/code&gt;。&lt;strong&gt;Tool 定义、messages、trim、save nudge 全不变。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key 只放 &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt;，不要写进博文、不要 commit；发 DEV 时用占位符。&lt;/li&gt;
&lt;li&gt;云端按 token 计费；联网 Tool 会把 &lt;strong&gt;长网页摘要&lt;/strong&gt; 送回模型，费用和延迟都会上去 — 与第三篇 trim、Tool 内截断仍然相关。&lt;/li&gt;
&lt;li&gt;macOS 代理坑：&lt;code&gt;httpx.Client(trust_env=False)&lt;/code&gt; 继续用于 localhost Ollama；云端 HTTPS 一般不受影响。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CLI 启动时会打印：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM: deepseek-chat @ https://api.deepseek.com/v1 （API Key 已配置）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. 为什么单独做网页，而不是再加一个 Tool？
&lt;/h2&gt;

&lt;p&gt;调研 Agent 的「产品感」来自 &lt;strong&gt;可观测的多步过程&lt;/strong&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step → 计划调哪些 Tool → 每次 Observation → sources 变长 → 最终答案 / Saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CLI 的 &lt;code&gt;--trace&lt;/code&gt; 已经包含这些信息，但对非终端用户不友好。网页要做的是：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Loop&lt;/strong&gt;（已有）&lt;/td&gt;
&lt;td&gt;推理、Tool、state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;on_event 回调&lt;/strong&gt;（新增）&lt;/td&gt;
&lt;td&gt;每步向 UI 推送结构化事件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;FastAPI + SSE&lt;/strong&gt;（新增）&lt;/td&gt;
&lt;td&gt;HTTP 收问题、流式推事件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;静态页&lt;/strong&gt;（新增）&lt;/td&gt;
&lt;td&gt;聊天气泡 + 时间线 + 来源列表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;没有&lt;/strong&gt; 为 UI 再写一套 Agent，也没有 LangGraph。&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Loop 加钩子：&lt;code&gt;on_event&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;在 &lt;code&gt;agent/loop.py&lt;/code&gt; 的 &lt;code&gt;_agent_loop&lt;/code&gt; 增加可选参数 &lt;code&gt;on_event(kind, data)&lt;/code&gt;，例如：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;event&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;step&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;第几轮 LLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assistant_tools&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;本轮要调用的 Tool 名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tool_start&lt;/code&gt; / &lt;code&gt;tool_end&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;参数与结果预览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;done&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最终答案 + &lt;code&gt;sources&lt;/code&gt; 快照&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CLI 不传 &lt;code&gt;on_event&lt;/code&gt;，行为与以前一致。网页在后台线程里跑 &lt;code&gt;run_agent_state(..., on_event=...)&lt;/code&gt;，事件进 &lt;code&gt;queue&lt;/code&gt;，由 &lt;strong&gt;Server-Sent Events&lt;/strong&gt; 推到浏览器。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 当前是 &lt;strong&gt;同步 Loop + 线程&lt;/strong&gt;；没有做成 async LLM。对个人本地台足够，公网高并发不是目标。&lt;/p&gt;




&lt;h2&gt;
  
  
  4. 网页调研台结构
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web/
  server.py      # FastAPI：/api/chat（SSE）、/api/config、/api/session
  static/
    index.html   # 布局：左对话、右轨迹+来源
    app.js       # fetch SSE、渲染 timeline
    app.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;会话：&lt;/strong&gt; 内存里 &lt;code&gt;session_id → AgentState&lt;/code&gt;；勾选「多轮」则同一 session 连续 &lt;code&gt;append_user&lt;/code&gt;（类似 &lt;code&gt;main.py -i&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;默认单轮：&lt;/strong&gt; 每次提问 &lt;code&gt;reset&lt;/code&gt; state，避免误把上一条任务混进 sources。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;绑定：&lt;/strong&gt; &lt;code&gt;WEB_HOST=127.0.0.1&lt;/code&gt;、&lt;code&gt;WEB_PORT=8765&lt;/code&gt;，默认只监听本机。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;启动：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="c"&gt;# .env 配好 LLM_* 或继续用 Ollama&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; web.server
&lt;span class="c"&gt;# 浏览器打开 http://127.0.0.1:8765&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;页眉会显示当前 &lt;strong&gt;LLM 端点&lt;/strong&gt;（不含 Key）。右侧在 Tool 执行后刷新 &lt;strong&gt;来源&lt;/strong&gt; 列表，与第五篇 &lt;code&gt;sources&lt;/code&gt; 一致。&lt;/p&gt;

&lt;p&gt;本机实测（&lt;code&gt;LLM_PROVIDER=glm&lt;/code&gt; + 环境变量 &lt;code&gt;GLM_API_KEY&lt;/code&gt;，提问「调研一下最新的 ai 新闻」）：模型第 1 轮调用 &lt;code&gt;search_hn&lt;/code&gt;，第 2 轮汇总；右侧可见 Step / Tool / &lt;strong&gt;来源 5&lt;/strong&gt;，与 CLI trace 一致。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjdysluur5io00qlobzei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjdysluur5io00qlobzei.png" alt="网页调研台：左侧回答、右侧运行轨迹与来源列表" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. 和 CLI 怎么选
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;建议&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;改 prompt、压测 trim、脚本化&lt;/td&gt;
&lt;td&gt;&lt;code&gt;main.py -v --trace&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;演示调研链、给非 CLI 用户试&lt;/td&gt;
&lt;td&gt;网页台&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7B 本地 + 偶发长报告&lt;/td&gt;
&lt;td&gt;本地搜 + &lt;strong&gt;云端 LLM&lt;/strong&gt; 成稿（同 Loop）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;公网部署&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;尚未做&lt;/strong&gt; 登录、限速、SSRF 加固 — 见第七篇边界&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;save 仍写 &lt;strong&gt;服务端&lt;/strong&gt; &lt;code&gt;reports/&lt;/code&gt;；网页不会代替 &lt;code&gt;save_markdown&lt;/code&gt;，只是更容易看到有没有走到 &lt;code&gt;Saved:&lt;/code&gt; 预览。&lt;/p&gt;




&lt;h2&gt;
  
  
  6. 本机验收
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 智谱（Key 已在 ~/.zshrc 的 GLM_API_KEY 时）&lt;/span&gt;
&lt;span class="c"&gt;# .env: LLM_PROVIDER=glm  LLM_MODEL=glm-4-flash&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
python &lt;span class="nt"&gt;-m&lt;/span&gt; web.server

&lt;span class="c"&gt;# 或其它 OpenAI 兼容厂商&lt;/span&gt;
&lt;span class="c"&gt;# export LLM_BASE_URL=...  export LLM_API_KEY=...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;浏览器发一条窄 query，例如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;调研一下最新的 ai 新闻
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;或：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用 search_hn 列 3 条 AI 相关帖子，中文概括，并列出引用 URL。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;右侧应出现 Step → &lt;code&gt;search_hn&lt;/code&gt; → 来源增加 → 左侧最终回答。&lt;/p&gt;

&lt;p&gt;CLI 对照：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s1"&gt;'同上'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. 收束
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;换云模型&lt;/strong&gt; = 改 &lt;code&gt;LLM_*&lt;/code&gt;，Loop / Tool / sources / save &lt;strong&gt;不动&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;网页台&lt;/strong&gt; = &lt;code&gt;on_event&lt;/code&gt; + FastAPI SSE + 静态 UI，&lt;strong&gt;可视化同一套调研 Agent&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;安全默认&lt;/strong&gt; = 本机绑定 + Key 在 &lt;code&gt;.env&lt;/code&gt;；上公网前看第七篇。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;代码：&lt;a href="https://github.com/zealot2002/myagent" rel="noopener noreferrer"&gt;zealot2002/myagent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  下一篇
&lt;/h2&gt;

&lt;p&gt;《&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/07-%E4%B8%8E%E5%95%86%E7%94%A8Agent%E8%BF%98%E5%B7%AE%E4%BB%80%E4%B9%88.md" rel="noopener noreferrer"&gt;从零手写 Agent（七）：和商用 Agent 还差什么——个人调研 Agent 的边界&lt;/a&gt;》——在「能用的 CLI + 网页原型」之上，对照商用能力地图与刻意不做的事。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>agents</category>
      <category>fastapi</category>
      <category>openai</category>
      <category>research</category>
    </item>
    <item>
      <title>从零手写 Agent（五）：调研报告、引用与落盘</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:07:21 +0000</pubDate>
      <link>https://dev.to/zealot2002/cong-ling-shou-xie-agentwu-diao-yan-bao-gao-yin-yong-yu-luo-pan-loop-reng-bu-bian-41ch</link>
      <guid>https://dev.to/zealot2002/cong-ling-shou-xie-agentwu-diao-yan-bao-gao-yin-yong-yu-luo-pan-loop-reng-bu-bian-41ch</guid>
      <description>&lt;h1&gt;
  
  
  从零手写 Agent（五）：调研报告、引用与落盘
&lt;/h1&gt;

&lt;p&gt;第四篇之后，Agent 已经能 &lt;strong&gt;search → fetch → 多步回答&lt;/strong&gt;。用户要的往往是另一件事：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;写成 Markdown，带上参考资料，&lt;strong&gt;保存到 &lt;code&gt;reports/xxx.md&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;终端里一段口语摘要不算交付。第五篇在 &lt;strong&gt;同一个 &lt;code&gt;_agent_loop&lt;/code&gt;&lt;/strong&gt; 上补 &lt;strong&gt;落盘&lt;/strong&gt; 和 &lt;strong&gt;可核对引用&lt;/strong&gt;，核心新增：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;save_markdown
sources（state + system 注入）
Loop：save nudge / 必要时强制 save_markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;（仓库里还扩展了 &lt;code&gt;search_hn&lt;/code&gt;、&lt;code&gt;fetch_rss&lt;/code&gt;，与 &lt;code&gt;search_web&lt;/code&gt; 一样会把 URL 收进 &lt;code&gt;sources&lt;/code&gt;。）&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 第四篇之后，还差哪一层？
&lt;/h2&gt;

&lt;p&gt;Web Research 原型：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;问题 → search → fetch → answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;调研 &lt;strong&gt;交付&lt;/strong&gt; 还需要：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;层次&lt;/th&gt;
&lt;th&gt;目标&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;结构化正文&lt;/td&gt;
&lt;td&gt;摘要 / 发现 / 局限 / 参考文献&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可核对引用&lt;/td&gt;
&lt;td&gt;URL 来自本轮 Tool 结果，而不是模型「回忆」&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;落盘&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cat reports/foo.md&lt;/code&gt; 能看到文件&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;最小链路：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search / fetch / hn / rss
        ↓
组织 Markdown
        ↓
save_markdown
        ↓
reports/*.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; Loop 仍不需要为「报告」单独写一套；变的是 Tool 列表、State 和 prompt。&lt;/p&gt;




&lt;h2&gt;
  
  
  2. 第一个坑：说了「保存」，磁盘却没有文件
&lt;/h2&gt;

&lt;p&gt;实测里很常见：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户：保存到 reports/python-313.md
Agent：（长报告）…… 结束
ls reports/   # 空
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;原因分两层：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;（1）没有 &lt;code&gt;save_markdown&lt;/code&gt; Tool&lt;/strong&gt; — 模型只能在对话里「假装保存」，程序写不了盘。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;（2）有 Tool，但模型选择不调用&lt;/strong&gt; — Tool 注册成功 ≠ 一定会 &lt;code&gt;tool_calls&lt;/code&gt;。Loop 在 &lt;strong&gt;没有 tool_calls&lt;/strong&gt; 时直接返回 assistant 正文并退出；&lt;strong&gt;不会&lt;/strong&gt;因为用户说了「保存」就自动写文件。&lt;/p&gt;

&lt;p&gt;所以交付要同时做：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;提供 &lt;code&gt;save_markdown&lt;/code&gt;（真实 IO）&lt;/li&gt;
&lt;li&gt;在 prompt 里写清：用户指定路径时必须 save&lt;/li&gt;
&lt;li&gt;必要时 Loop &lt;strong&gt;save nudge&lt;/strong&gt; 或 &lt;strong&gt;&lt;code&gt;tool_choice&lt;/code&gt; 强制 &lt;code&gt;save_markdown&lt;/code&gt; 一轮&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这和「模型够不够聪明」有关，更是 &lt;strong&gt;完成条件写在代码里&lt;/strong&gt; 的问题。&lt;/p&gt;




&lt;h2&gt;
  
  
  3. &lt;code&gt;save_markdown&lt;/code&gt;：沙箱与 Tool 契约
&lt;/h2&gt;

&lt;p&gt;实现要点（细节见仓库 &lt;code&gt;agent/tools/save_markdown.py&lt;/code&gt;）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;只允许写入 &lt;strong&gt;&lt;code&gt;reports/&lt;/code&gt;&lt;/strong&gt; 下相对路径，禁止 &lt;code&gt;..&lt;/code&gt; 逃逸&lt;/li&gt;
&lt;li&gt;参数：&lt;code&gt;filename&lt;/code&gt;、&lt;code&gt;content&lt;/code&gt;（整份 Markdown 由模型生成）&lt;/li&gt;
&lt;li&gt;成功时 Tool 返回里带 &lt;strong&gt;&lt;code&gt;Saved:&lt;/code&gt;&lt;/strong&gt; 路径，便于 trace 验收&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 大段正文走 &lt;strong&gt;一次 tool 参数&lt;/strong&gt;，7B 可能截断或偷懒；复杂报告可拆「先 fetch 凑 sources → 再 save」，或接受「摘要版报告 + 引用列表」。&lt;/p&gt;




&lt;h2&gt;
  
  
  4. &lt;code&gt;sources&lt;/code&gt;：引用从哪来
&lt;/h2&gt;

&lt;p&gt;模型会改写标题、补假 URL。更稳妥的规则：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;参考文献里的 URL，应优先来自本轮 Agent 实际见过的搜索/抓取结果。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;做法：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AgentState.sources&lt;/code&gt;&lt;/strong&gt; — 每次 Tool 返回后 &lt;code&gt;ingest_tool_sources(name, args, result)&lt;/code&gt;，从 &lt;code&gt;search_web&lt;/code&gt; / &lt;code&gt;search_hn&lt;/code&gt; / &lt;code&gt;fetch_rss&lt;/code&gt; / &lt;code&gt;fetch_url&lt;/code&gt; 等解析 URL，去重&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sync_system_message()&lt;/code&gt;&lt;/strong&gt; — 每轮调用 LLM 前，把 &lt;strong&gt;【本轮已收集来源】&lt;/strong&gt; 写进 system（条数可配置上限）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;prompt&lt;/strong&gt; — 要求正文引用与列表一致，无来源时不得编造链接&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;商用产品往往还有成稿 URL 白名单校验；个人版先到 &lt;strong&gt;sources 注入 + trace 肉眼核对&lt;/strong&gt;，第六篇会写边界。&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Prompt 与 Loop：save nudge
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;agent/prompt.py&lt;/code&gt; 里典型约束：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户要求保存 → 必须调用 &lt;code&gt;save_markdown&lt;/code&gt;，不要只在聊天里贴全文&lt;/li&gt;
&lt;li&gt;报告结构：标题、摘要、发现、局限、参考文献&lt;/li&gt;
&lt;li&gt;HN/RSS 类任务：先 &lt;code&gt;search_hn&lt;/code&gt; 或 &lt;code&gt;fetch_rss&lt;/code&gt;，再整理&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;agent/loop.py&lt;/code&gt; 在检测到「用户要保存但本轮未 save」时，可 &lt;strong&gt;追加一轮 nudge&lt;/strong&gt;；仍不 save 时 &lt;strong&gt;&lt;code&gt;force_tool_name="save_markdown"&lt;/code&gt;&lt;/strong&gt; 强制调用一次（见 &lt;code&gt;agent/llm.py&lt;/code&gt; 的 &lt;code&gt;tool_choice&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 强制 save 只解决「不调用 Tool」；&lt;strong&gt;content 质量仍取决于模型与前面 fetch 是否充分&lt;/strong&gt;。&lt;/p&gt;




&lt;h2&gt;
  
  
  6. 与第四篇实测的衔接
&lt;/h2&gt;

&lt;p&gt;第四篇 §4.2 已说明：&lt;strong&gt;Tool 成功 ≠ 任务成功&lt;/strong&gt;（宽 query、门户首页、只给链接）。第五篇在此基础上：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;窄 query + 多源&lt;/strong&gt;（如 &lt;code&gt;search_hn&lt;/code&gt;、具体站点 &lt;code&gt;search_web&lt;/code&gt;）再 save，通过率更高&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;「列 HN 头条」&lt;/strong&gt; 不等于自动落盘 — 除非用户要 save 或 prompt/nudge 触发&lt;/li&gt;
&lt;li&gt;trace 里同时看：&lt;code&gt;tool_calls&lt;/code&gt;、&lt;code&gt;【本轮已收集来源】&lt;/code&gt;、&lt;code&gt;Saved:&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. 本机验收
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen2.5:7b
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# 专题 + 落盘（示例）&lt;/span&gt;
python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s1"&gt;'调研 Python 3.13 相对 3.12 的主要新特性，写 Markdown，参考文献用本轮来源，保存到 reports/python-313.md'&lt;/span&gt;

&lt;span class="nb"&gt;ls &lt;/span&gt;reports/
&lt;span class="nb"&gt;cat &lt;/span&gt;reports/python-313.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;交互调试：&lt;code&gt;python main.py -i --trace&lt;/code&gt;。&lt;/p&gt;




&lt;h2&gt;
  
  
  8. 收束
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;概念&lt;/th&gt;
&lt;th&gt;要点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;交付&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;save_markdown&lt;/code&gt; + &lt;code&gt;reports/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;引用&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sources&lt;/code&gt; 注入 system，减少假链&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loop&lt;/td&gt;
&lt;td&gt;不变；完成条件靠 nudge / 强制 Tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型&lt;/td&gt;
&lt;td&gt;7B 可能跳过 save 或简写 — 用 trace 验收&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;代码：&lt;a href="https://github.com/zealot2002/myagent" rel="noopener noreferrer"&gt;zealot2002/myagent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  下一篇
&lt;/h2&gt;

&lt;p&gt;《&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/06-%E4%BA%91%E6%A8%A1%E5%9E%8B%E4%B8%8E%E7%BD%91%E9%A1%B5%E8%B0%83%E7%A0%94%E5%8F%B0.md" rel="noopener noreferrer"&gt;从零手写 Agent（六）：云模型 API Key 与网页调研台——Loop 仍不变&lt;/a&gt;》——&lt;code&gt;LLM_*&lt;/code&gt; 换云端推理，FastAPI + SSE 把调研链可视化。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>agents</category>
      <category>research</category>
      <category>citations</category>
      <category>markdown</category>
    </item>
    <item>
      <title>从零手写 Agent（四）：多 Tool 与联网检索</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:06:43 +0000</pubDate>
      <link>https://dev.to/zealot2002/cong-ling-shou-xie-agentsi-duo-tool-yu-lian-wang-jian-suo-loop-huan-shi-na-ge-loop-a62</link>
      <guid>https://dev.to/zealot2002/cong-ling-shou-xie-agentsi-duo-tool-yu-lian-wang-jian-suo-loop-huan-shi-na-ge-loop-a62</guid>
      <description>&lt;h1&gt;
  
  
  从零手写 Agent（四）：多 Tool 与联网检索
&lt;/h1&gt;

&lt;p&gt;前面三篇，我基本把一个 Agent 最核心的骨架搭出来了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;第一篇：Tool Calling，让模型可以调用外部能力。&lt;/li&gt;
&lt;li&gt;第二篇：messages，让 Agent 有了短期记忆。&lt;/li&gt;
&lt;li&gt;第三篇：上下文超长，开始处理 Context Window 和历史消息治理。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;做到这里，下一步很自然：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果我再加两个 Tool，让 Agent 能搜索互联网、读取网页，是不是要继续给 Loop 加 &lt;code&gt;if/else&lt;/code&gt;？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;答案是：&lt;strong&gt;不用。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这一篇我给之前的 Calculator Agent 再加两个 Tool：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search_web&lt;/code&gt;：搜索互联网&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch_url&lt;/code&gt;：读取网页&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最终变成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户问题
   ↓
LLM
   ↓
选择 Tool
   ├── calculator
   ├── search_web
   └── fetch_url
   ↓
Tool 执行
   ↓
结果放回 messages
   ↓
LLM 再判断
   ↓
继续 Tool / 直接回答
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loop 还是之前那个 Loop。&lt;/p&gt;

&lt;p&gt;真正发生变化的是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tool 越来越多以后，Agent 的能力边界主要由 Tool Registry 决定，而不是由 Loop 决定。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 为什么还是同一个 Loop？
&lt;/h2&gt;

&lt;p&gt;之前的核心代码大概就是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_STEPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;prepare_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ALL_TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;现在增加 &lt;code&gt;search_web&lt;/code&gt; 和 &lt;code&gt;fetch_url&lt;/code&gt;，这里基本不用动。&lt;/p&gt;

&lt;p&gt;只需要：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ALL_TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;CALCULATOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SEARCH_WEB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FETCH_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ALL_TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型自己决定：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;要不要搜索？
    ↓
search_web

拿到 URL 了
    ↓
fetch_url

还需要计算
    ↓
calculator

信息已经够了
    ↓
直接回答
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以从 Agent Loop 的角度看：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool 变多
    ↓
Registry 变大
    ↓
模型可选择的动作变多
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而不是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool 变多
    ↓
Loop 增加大量 if/else
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这其实是这一篇最重要的结论。&lt;/p&gt;




&lt;h1&gt;
  
  
  2. 多 Tool 的关键：先把能力边界划清楚
&lt;/h1&gt;

&lt;p&gt;这次增加的两个 Tool 看起来都和“联网”有关，但我没有把它们合成一个：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web_tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而是拆成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
fetch_url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;原因很简单：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;搜索和读取网页，本来就是两个不同的动作。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;输入&lt;/th&gt;
&lt;th&gt;输出&lt;/th&gt;
&lt;th&gt;适合解决&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calculator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数学表达式&lt;/td&gt;
&lt;td&gt;数值&lt;/td&gt;
&lt;td&gt;精确计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;搜索词&lt;/td&gt;
&lt;td&gt;标题、URL、摘要&lt;/td&gt;
&lt;td&gt;不知道去哪里找&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fetch_url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;完整 URL&lt;/td&gt;
&lt;td&gt;网页正文&lt;/td&gt;
&lt;td&gt;已经知道去哪看&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;例如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“Python 3.13 是什么时候发布的？”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;一开始并不知道哪个页面有答案：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;拿到：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python 3.13.0
https://www.python.org/downloads/release/python-3130/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这时候才适合：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch_url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;去读取具体页面。&lt;/p&gt;

&lt;p&gt;如果一开始就让 &lt;code&gt;fetch_url&lt;/code&gt; 负责搜索，就会把“搜索”和“读取”混成一个能力，Tool 的职责越来越模糊。&lt;/p&gt;

&lt;p&gt;所以这里有一个很实用的原则：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一个 Tool 最好表达一个清晰的动作，而不是把一整段工作流塞进 Tool。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2.1 &lt;code&gt;search_web&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;给模型看的 Tool 描述大致是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用于搜索互联网信息、新闻、资料和网页链接。

不要用于：
- 纯数学计算
- 用户已经提供完整 URL 的情况
- 普通概念问答
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;参数：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Python 3.13 release date"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;其中：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;是搜索词。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max_results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;限制返回结果数量，当前范围：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 ~ 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;返回给模型的是普通字符串，例如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1] Python 3.13.0
URL: https://www.python.org/downloads/release/python-3130/
Snippet: Python 3.13.0 was released...

---

[2] What's New In Python 3.13
URL: ...
Snippet: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最后还是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tool"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以对 Agent Loop 来说，搜索结果没有特殊待遇。&lt;/p&gt;

&lt;p&gt;它就是一次 Observation。&lt;/p&gt;

&lt;p&gt;如果搜索失败，也只是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: search failed (...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型拿到这个结果之后，可以：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;换一个搜索词重试&lt;/li&gt;
&lt;li&gt;换一个 Tool&lt;/li&gt;
&lt;li&gt;或者直接告诉用户当前无法获取&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Loop 本身不需要知道“搜索失败了”。&lt;/p&gt;




&lt;h2&gt;
  
  
  2.2 &lt;code&gt;fetch_url&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fetch_url&lt;/code&gt; 的职责更加单一：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;用户或者前一步搜索已经给出了 URL，现在把这个网页内容读回来。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;参数：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.python.org/"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;当前只接受：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://
https://
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;返回的是经过简单处理后的网页文本。&lt;/p&gt;

&lt;p&gt;HTML 里的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;script&amp;gt;
&amp;lt;style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;等内容会被去掉。&lt;/p&gt;

&lt;p&gt;同时限制返回长度，大约：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8000 字符
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;超过以后：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...(fetch truncated in tool)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这里有两个原因。&lt;/p&gt;

&lt;p&gt;第一，网页本身可能非常长。&lt;/p&gt;

&lt;p&gt;第二，也是第三篇刚处理过的问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tool 返回太长，最终还是会进入 messages，继续消耗 Context Window。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以现在其实形成了两层控制：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch_url
    ↓
先限制网页输出长度
    ↓
messages
    ↓
prepare_messages()
    ↓
必要时继续做 Tool 内容截断 / 历史滑窗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也就是说，第三篇的上下文治理并没有因为增加 Web Tool 而失效，反而变得更重要了。&lt;/p&gt;




&lt;h1&gt;
  
  
  3. 三个 Tool 怎么注册？
&lt;/h1&gt;

&lt;p&gt;最终：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ALL_TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;CALCULATOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SEARCH_WEB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FETCH_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;每一个 Tool 还是之前的结构：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_web&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;...,&lt;/span&gt;
    &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;search_web&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent Loop 只需要：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;至于：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web 到底怎么搜？
fetch_url 到底怎么抓？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这些都属于 Tool 自己的事情。&lt;/p&gt;

&lt;p&gt;目录结构现在大概变成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent/
├── tools/
│   ├── calculator.py
│   ├── search_web.py
│   ├── fetch_url.py
│   └── __init__.py
├── loop.py
└── state.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;依赖增加：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ddgs
httpx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;相关环境变量：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SEARCH_TIMEOUT_SEC
SEARCH_BUDGET_SEC
FETCH_MAX_BYTES
FETCH_TIMEOUT_SEC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  4. &lt;code&gt;search_web&lt;/code&gt; 里面到底怎么实现？
&lt;/h1&gt;

&lt;p&gt;这里有一个容易混淆的地方：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent 是多 Tool，不代表一个 Tool 内部只能访问一个数据源。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我现在暴露给模型的仍然只有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但 &lt;code&gt;search_web&lt;/code&gt; 内部可以尝试多个搜索来源。&lt;/p&gt;

&lt;p&gt;当前学习版采用的是&lt;strong&gt;串行 Failover&lt;/strong&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ddgs
  ↓ 失败
DuckDuckGo Instant Answer
  ↓ 失败
Wikipedia
  ↓ 失败
Qwant
  ↓ 失败
SearXNG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;哪个先成功，就返回哪个结果。&lt;/p&gt;

&lt;p&gt;这样做的目的不是实现一个完整搜索引擎，而是让这个小项目在没有 API Key 的情况下，也能尽量跑起来。&lt;/p&gt;

&lt;p&gt;单个来源有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SEARCH_TIMEOUT_SEC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;整个搜索还有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SEARCH_BUDGET_SEC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;避免所有来源轮流超时，把 Agent 卡很久。&lt;/p&gt;

&lt;p&gt;运行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"Python 3.13 release date"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;可以看到：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search provider=ddgs success
search results=...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;或者：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search provider=ddgs failed
search provider=wikipedia success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这些信息属于日志，不应该直接塞进模型的 Observation。&lt;/p&gt;

&lt;p&gt;模型只需要看到统一格式：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;title
url
snippet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这也是为什么多源搜索最好封装在一个 Tool 里面。&lt;/p&gt;




&lt;h2&gt;
  
  
  4.1 为什么不直接暴露多个搜索 Tool？
&lt;/h2&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_duckduckgo
search_wikipedia
search_bing
search_qwant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;看起来很灵活，但模型一下子多了很多选择。&lt;/p&gt;

&lt;p&gt;而这些 Tool 的本质动作其实都是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query → 搜索结果
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;更合理的边界是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;模型
 ↓
search_web
 ↓
内部决定数据源
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;未来想从串行改成并发，也应该改：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;内部，而不是修改 Agent Loop。&lt;/p&gt;




&lt;h1&gt;
  
  
  5. 串行还是并行？
&lt;/h1&gt;

&lt;p&gt;如果继续往下做，多源搜索很自然会遇到这个问题。&lt;/p&gt;

&lt;p&gt;现在是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
↓
失败
B
↓
失败
C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最简单，但延迟可能比较高。&lt;/p&gt;

&lt;p&gt;另一种方式是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        ┌─ A
query ──┼─ B
        └─ C
             ↓
        merge + dedupe
             ↓
          ranking
             ↓
        SearchHit[]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;例如内部统一成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchHit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;snippet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;不同搜索源负责：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;原始响应
   ↓
adapter
   ↓
SearchHit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最后再统一格式化成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;role: tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这是后面如果要继续升级时比较自然的方向。&lt;/p&gt;

&lt;p&gt;但这篇的学习版没有把复杂度继续往上堆，仍然采用串行 Failover。&lt;/p&gt;




&lt;h1&gt;
  
  
  6. 多 Tool 真正跑起来是什么样？
&lt;/h1&gt;

&lt;p&gt;假设用户问：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;查一下 Python 官方站是做什么的，并把 123*456 算出来。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;一个可能的轨迹是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
LLM
 ↓
search_web
 ↓
搜索结果
 ↓
LLM
 ↓
fetch_url
 ↓
网页内容
 ↓
LLM
 ↓
calculator
 ↓
56088
 ↓
LLM
 ↓
Final Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;注意：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这个顺序不是写死的。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;模型也可能：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculator
 ↓
search_web
 ↓
fetch_url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;甚至只搜索，不抓取网页。&lt;/p&gt;

&lt;p&gt;所以 Agent Loop 的职责依然只是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM → Tool → Observation → LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;至于下一步是什么，由模型决定。&lt;/p&gt;




&lt;h1&gt;
  
  
  7. 实际测试：Tool 能调用，不代表结果就好
&lt;/h1&gt;

&lt;p&gt;这一部分我觉得比单纯展示代码更有价值。&lt;/p&gt;

&lt;p&gt;因为加上联网 Tool 后，很容易产生一个错觉：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“现在 Agent 会搜索了，所以它已经是 Research Agent 了。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;实际并不是。&lt;/p&gt;

&lt;p&gt;我用：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen2.5:3b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;做了两个测试。&lt;/p&gt;




&lt;h2&gt;
  
  
  7.1 测试一：直接问“今日新闻”
&lt;/h2&gt;

&lt;p&gt;执行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"今日新闻摘要"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型实际发出的搜索词是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Today's news summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;并且：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max_results=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ddgs&lt;/code&gt; 成功返回了结果，但结果比较尴尬：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Google News
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;页面本身更像一个新闻门户入口，摘要主要是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;登录
地区
导航
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最终 Agent 基本变成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;这里有一个今日新闻链接……
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也就是说：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool 调用成功
        ≠
得到高质量研究结果
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;问题并不是网络失败。&lt;/p&gt;

&lt;p&gt;而是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户问题太宽
    ↓
模型生成了英文宽泛 Query
    ↓
只取 1 条结果
    ↓
命中了门户首页
    ↓
没有继续 fetch
    ↓
最终只能给链接
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7.2 测试二：把搜索目标说具体
&lt;/h2&gt;

&lt;p&gt;换一个：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s1"&gt;'用 search_web 检索「新华社 今日要闻」或「BBC world news today」，根据返回的标题和摘要用中文列 3 条，不要只给链接。'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这次模型搜索：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;新华社 今日要闻
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;返回 5 条结果。&lt;/p&gt;

&lt;p&gt;结果里出现了比较明确的新华社页面：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;标题
URL
Snippet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最后模型能够根据这些摘要整理出 3 条中文内容。&lt;/p&gt;

&lt;p&gt;这次没有调用 &lt;code&gt;fetch_url&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;其实已经够解释一个很重要的问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;搜索质量很大程度取决于模型给搜索 Tool 的 Query，以及搜索结果本身是什么类型。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;两个测试对比：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;测试一&lt;/th&gt;
&lt;th&gt;测试二&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Query&lt;/td&gt;
&lt;td&gt;今日新闻&lt;/td&gt;
&lt;td&gt;新华社 今日要闻&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Query 语言&lt;/td&gt;
&lt;td&gt;英文&lt;/td&gt;
&lt;td&gt;中文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max_results&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;结果&lt;/td&gt;
&lt;td&gt;新闻门户&lt;/td&gt;
&lt;td&gt;具体新闻页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最终结果&lt;/td&gt;
&lt;td&gt;基本给链接&lt;/td&gt;
&lt;td&gt;能整理 3 条摘要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;所以这里暂时不能把第二个测试说成“已经可以做今日新闻调研”。&lt;/p&gt;

&lt;p&gt;它只是说明：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
    ↓
返回质量还可以的 Observation
    ↓
小模型能够继续加工
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;另外测试里还出现了一个很典型的问题：&lt;/p&gt;

&lt;p&gt;模型把 Tool 返回的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;权威快报
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;写成了：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;权力建报
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这意味着：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tool 返回的内容进入模型以后，模型仍然可能改写甚至产生错误。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这也是下一篇要解决的引用问题。&lt;/p&gt;




&lt;h1&gt;
  
  
  8. 这里到底算不算 Research Agent？
&lt;/h1&gt;

&lt;p&gt;如果只看能力链路：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户问题
 ↓
search
 ↓
fetch
 ↓
多步推理
 ↓
answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;现在已经具备一个最小的 Web Research Agent 雏形。&lt;/p&gt;

&lt;p&gt;但我更愿意把它叫：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;学习版 Research Agent，而不是完整的调研系统。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;目前已经有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;多 Tool&lt;/li&gt;
&lt;li&gt;Web Search&lt;/li&gt;
&lt;li&gt;网页读取&lt;/li&gt;
&lt;li&gt;多步 Tool Calling&lt;/li&gt;
&lt;li&gt;短期记忆&lt;/li&gt;
&lt;li&gt;Context Trim&lt;/li&gt;
&lt;li&gt;Trace / Debug&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;但距离真正的“调研交付”还差几个关键东西。&lt;/p&gt;

&lt;h3&gt;
  
  
  第一，缺少明确的调研流程
&lt;/h3&gt;

&lt;p&gt;现在主要依赖模型临场决定：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;搜什么
搜几次
什么时候 fetch
什么时候停止
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;还没有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;规划
→ 搜索
→ 补充搜索
→ 交叉验证
→ 汇总
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这样的显式流程。&lt;/p&gt;

&lt;h3&gt;
  
  
  第二，来源没有被结构化保存
&lt;/h3&gt;

&lt;p&gt;现在：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;返回：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;标题 + URL + 摘要
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型读完之后自己组织答案。&lt;/p&gt;

&lt;p&gt;如果模型改写错了，Agent 很难约束它：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;这个说法到底来自哪个 URL？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  第三，还没有真正的报告产物
&lt;/h3&gt;

&lt;p&gt;现在最终结果还是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response.content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;如果要做成真正的调研 Agent，应该有类似：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;summary
sections
references
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这样的结构，并且最终能保存成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reports/xxx.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这就是下一篇准备解决的问题。&lt;/p&gt;




&lt;h1&gt;
  
  
  9. 多 Tool 以后，Agent 的边界到底在哪里？
&lt;/h1&gt;

&lt;p&gt;做到这里，可以重新看一下整个 Agent。&lt;/p&gt;

&lt;p&gt;第一篇的时候：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
+
calculator
+
Loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;现在：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
+
calculator
+
search_web
+
fetch_url
+
Loop
+
State
+
Context Governance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但 Loop 还是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_STEPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;prepare_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ALL_TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以我现在越来越倾向于把 Agent 理解成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent
=
LLM
+
Tool Registry
+
Loop
+
State
+
Context Governance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;其中：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool Registry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;决定 Agent &lt;strong&gt;能做什么&lt;/strong&gt;。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;决定当前 &lt;strong&gt;想做什么&lt;/strong&gt;。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;负责把：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;想做
→ 执行
→ 观察
→ 再想
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;串起来。&lt;/p&gt;

&lt;p&gt;而：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;State / Context Governance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;负责让这个过程不要因为历史消息和 Tool 输出越来越长而失控。&lt;/p&gt;




&lt;h1&gt;
  
  
  10. 本地运行
&lt;/h1&gt;

&lt;p&gt;安装依赖：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;启动模型：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen2.5:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;直接测试搜索：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from agent.tools.search_web import search_web; print(search_web('Python 3.13 release date', 2))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;测试网页读取：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from agent.tools.fetch_url import fetch_url; print(fetch_url('https://www.python.org/downloads/release/python-3130/')[:600])"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;让 Agent 自己调用搜索：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s1"&gt;'请用 search_web 查 Python 3.13 正式版发布日期，并写出结果里的来源标题。'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;直接测试 &lt;code&gt;fetch_url&lt;/code&gt;：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s1"&gt;'请用 fetch_url 读取 https://www.python.org/downloads/release/python-3130/ 并中文概括发布日期。'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;测试多 Tool：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"Python 3.12 有什么新特性？简述并给出来源。"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;如果需要交互调试：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;搜索相关功能依赖网络。&lt;/p&gt;

&lt;p&gt;另外，如果终端出现：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dquote&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这类提示，通常是 Shell 引号没有闭合。命令里的整句提示建议用英文单引号包起来。&lt;/p&gt;




&lt;h2&gt;
  
  
  11. 收束
&lt;/h2&gt;

&lt;p&gt;演进线：Tool → &lt;code&gt;messages&lt;/code&gt; → trim → &lt;strong&gt;多 Tool 联网&lt;/strong&gt;。第四篇一句话：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;加能力 = 加 Tool 并注册；Loop 形状不变。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;search_web&lt;/code&gt;（及后续的 &lt;code&gt;search_hn&lt;/code&gt;、&lt;code&gt;fetch_rss&lt;/code&gt;）解决「去哪找」；&lt;code&gt;fetch_url&lt;/code&gt; 解决「打开已知 URL」。失败、超时、HTML 简抽、结果截断都封在 Tool 内。§4.2 的对比说明：&lt;strong&gt;Tool 成功 ≠ 研究结果合格&lt;/strong&gt; — query 宽窄与是否继续 fetch 同样关键。&lt;/p&gt;

&lt;p&gt;边界应划清：&lt;strong&gt;Tool Contract、Registry、State、Context Governance&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;Repo：&lt;a href="https://github.com/zealot2002/myagent" rel="noopener noreferrer"&gt;zealot2002/myagent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  下一篇
&lt;/h2&gt;

&lt;p&gt;《&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/05-%E8%B0%83%E7%A0%94%E6%8A%A5%E5%91%8A%E4%B8%8E%E5%BC%95%E7%94%A8.md" rel="noopener noreferrer"&gt;从零手写 Agent（五）：调研报告、引用与落盘——Loop 仍不变&lt;/a&gt;》——&lt;code&gt;save_markdown&lt;/code&gt;、&lt;code&gt;sources&lt;/code&gt;、save nudge。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  附录：当前 Tool Registry（发表时）
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ALL_TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;CALCULATOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SEARCH_WEB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SEARCH_HN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FETCH_RSS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FETCH_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SAVE_MARKDOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>agents</category>
      <category>websearch</category>
      <category>toolcalling</category>
      <category>python</category>
    </item>
    <item>
      <title>从零手写 Agent（三）：上下文超长，别等模型替你截断</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:06:03 +0000</pubDate>
      <link>https://dev.to/zealot2002/cong-ling-shou-xie-agentsan-shang-xia-wen-chao-chang-bie-deng-mo-xing-ti-ni-jie-duan-1fld</link>
      <guid>https://dev.to/zealot2002/cong-ling-shou-xie-agentsan-shang-xia-wen-chao-chang-bie-deng-mo-xing-ti-ni-jie-duan-1fld</guid>
      <description>&lt;h1&gt;
  
  
  从零手写 Agent（三）：上下文超长，别等模型替你截断
&lt;/h1&gt;

&lt;p&gt;第二篇里，我做了一个比较极端的压测。&lt;/p&gt;

&lt;p&gt;把 Ollama 的 &lt;code&gt;num_ctx&lt;/code&gt; 调得很小，然后不断往 &lt;code&gt;messages&lt;/code&gt; 里塞内容。&lt;/p&gt;

&lt;p&gt;最后发现一个挺麻烦的问题：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;上下文不够的时候，Ollama 不一定报错，而是可能直接把前面的内容截掉。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;更麻烦的是，程序看起来一切正常。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;messages&lt;/code&gt; 还在，接口返回 200，Agent 也给了答案。&lt;/p&gt;

&lt;p&gt;只是模型其实已经看不到最早的那部分上下文了。&lt;/p&gt;

&lt;p&gt;所以第三篇，我不再等模型侧处理。&lt;/p&gt;

&lt;p&gt;这次直接把“上下文治理”放进自己的 Agent Loop：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;每次调用 &lt;code&gt;chat()&lt;/code&gt; 之前，先检查一下 messages，该截的截，该删的删。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;代码核心放在：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent/context.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后用同一个压测脚本，对比：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;什么都不做&lt;/li&gt;
&lt;li&gt;主动 trim&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;看看两种情况下，模型到底看到了什么。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 先明确：我要解决的不是“无限 Context”
&lt;/h2&gt;

&lt;p&gt;先看两种模式的区别。&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;不做处理&lt;/th&gt;
&lt;th&gt;主动 trim&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;messages&lt;/code&gt; 在进程里一直变长&lt;/td&gt;
&lt;td&gt;超过预算就删旧消息 / 截短 tool 正文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模型侧可能静默丢掉前面的内容&lt;/td&gt;
&lt;td&gt;我们自己决定删什么&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trace 看起来历史还在&lt;/td&gt;
&lt;td&gt;trace 和模型实际看到的内容一致&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不知道到底丢了什么&lt;/td&gt;
&lt;td&gt;system 里明确告诉模型有历史被省略&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;这里有一个很重要的目标：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;不是想办法让 Agent 拥有无限大的 Context。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Context 本身就是有限的。&lt;/p&gt;

&lt;p&gt;我要做的是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;在 Context 有限的情况下，让 Agent 的行为变得可预测。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;至少不要出现这种情况：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;程序：
“我这里明明还有 12 条 messages 啊。”

模型：
“我只看到了后面几条。”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  2. 先上两个最朴素的策略
&lt;/h1&gt;

&lt;p&gt;这版没有上什么复杂的 Context Manager。&lt;/p&gt;

&lt;p&gt;先做两个最基础的事情：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tool 内容太长，先截断&lt;/li&gt;
&lt;li&gt;messages 太长，删除旧消息&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;够用了。&lt;/p&gt;

&lt;h2&gt;
  
  
  2.1 Tool 内容太长，先截
&lt;/h2&gt;

&lt;p&gt;之前 Calculator 的 tool result 只有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;当然不需要处理。&lt;/p&gt;

&lt;p&gt;但以后如果 Agent 加了一个 &lt;code&gt;fetch&lt;/code&gt; 工具，情况就不一样了。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;role: tool

这里是一篇几万字的网页正文……
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这种内容如果原封不动塞进 Context，很容易把窗口吃掉。&lt;/p&gt;

&lt;p&gt;所以先对单条 &lt;code&gt;role: tool&lt;/code&gt; 做限制。&lt;/p&gt;

&lt;p&gt;默认：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONTEXT_TOOL_MAX_CHARS=2000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;超过 2000 字符，就把后面的内容截掉，并追加：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;…(content truncated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也就是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;原始：

[一大段 tool 返回结果……]

处理后：

[前 2000 字符……]
…(content truncated)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这个策略比较简单，但对后面的 Agent 很有用。&lt;/p&gt;

&lt;p&gt;尤其是联网、文件读取、搜索这类 Tool，返回内容往往比 Calculator 大很多。&lt;/p&gt;




&lt;h1&gt;
  
  
  3. messages 太长，再做滑动窗口
&lt;/h1&gt;

&lt;p&gt;Tool 自己截断之后，还有一个问题：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;整个 messages 还是会越来越长。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user
assistant
tool
assistant
user
assistant
tool
assistant
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;聊几十轮以后，总长度还是会超过 Context。&lt;/p&gt;

&lt;p&gt;所以第二层开始处理整个 messages。&lt;/p&gt;

&lt;p&gt;我这里做的是一个非常简单的滑动窗口。&lt;/p&gt;

&lt;p&gt;默认：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONTEXT_MAX_TOKENS=8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;先估算当前 messages 的 token 数。&lt;/p&gt;

&lt;p&gt;如果超过预算：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;保留最前面的 &lt;code&gt;system&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;从最老的普通消息开始删&lt;/li&gt;
&lt;li&gt;一直删到估算 token 回到预算以内&lt;/li&gt;
&lt;li&gt;如果真的删过历史，就在 system 末尾加一个提示&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;提示类似：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;【注意】更早的对话已因上下文长度限制被省略…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这里的 token 估算也没有搞得很复杂。&lt;/p&gt;

&lt;p&gt;直接：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这个和前面的压测脚本保持一致。&lt;/p&gt;

&lt;p&gt;当然，它不是 tokenizer 精确计算。&lt;/p&gt;

&lt;p&gt;但现在的目的主要是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;做相对稳定的容量控制和压测对比。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不是拿它当真正的 token billing。&lt;/p&gt;




&lt;h1&gt;
  
  
  4. 最关键的是：trim 到底放在哪里？
&lt;/h1&gt;

&lt;p&gt;这个位置其实比算法本身更重要。&lt;/p&gt;

&lt;p&gt;Agent Loop 大概是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;...:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;我现在把它改成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;...:&lt;/span&gt;
    &lt;span class="nf"&gt;prepare_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也就是说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;每一次调用 LLM 之前，都先做一次 Context 检查。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;核心代码就是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;prepare_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而且是&lt;strong&gt;就地修改 &lt;code&gt;state.messages&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;不是复制一份临时 messages 给模型。&lt;/p&gt;

&lt;p&gt;这一点后面调试的时候非常重要。&lt;/p&gt;

&lt;p&gt;因为如果模型下一轮看到的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
B
C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;那么 trace 最好也应该记录：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
B
C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而不是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A
B
C
D
E
F
G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后实际上模型根本没看到 &lt;code&gt;D~G&lt;/code&gt;。&lt;/p&gt;




&lt;h1&gt;
  
  
  5. 开始压测
&lt;/h1&gt;

&lt;p&gt;接下来就是最有意思的部分。&lt;/p&gt;

&lt;p&gt;我写了一个对比脚本：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/compare_context_trim.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也可以分别跑：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/simulate_context_window.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--num-ctx&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--chunk-chars&lt;/span&gt; 4000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-rounds&lt;/span&gt; 6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;开启 trim：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/simulate_context_window.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--num-ctx&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--chunk-chars&lt;/span&gt; 4000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-rounds&lt;/span&gt; 6 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--with-trim&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-tokens&lt;/span&gt; 1200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;为了让问题明显一点，我故意把：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;num_ctx=256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;压得非常小。&lt;/p&gt;

&lt;p&gt;每一轮都往上下文里灌：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[块 i]
+ 一大段重复文本
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最后统一问：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;最早是第几块？只答数字。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;理论上，如果模型能看到完整历史，答案应该是最早那一块。&lt;/p&gt;




&lt;h1&gt;
  
  
  6. 不做 trim：messages 还在，但模型已经看不全了
&lt;/h1&gt;

&lt;p&gt;先看不做 trim 的结果。&lt;/p&gt;

&lt;p&gt;第 6 轮：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- round 6: messages=12 approx_tokens≈6703 ---
OK  reply='3'
     usage: prompt=2373 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这里最值得注意的不是答案 &lt;code&gt;3&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;而是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages=12
approx_tokens≈6703
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;按照我们程序里的 messages 来看，历史还完整地在那里。&lt;/p&gt;

&lt;p&gt;但是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt_tokens≈2373
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;一直卡在一个比较小的范围。&lt;/p&gt;

&lt;p&gt;这说明什么？&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;引擎已经开始自己截上下文了。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;于是最后模型回答：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但真实最早的块并不是 &lt;code&gt;3&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;也就是说：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;程序认为：

[块1]
[块2]
[块3]
[块4]
[块5]
[块6]

模型实际上看到的：

        [块3]
[块4]
[块5]
[块6]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这就是第二篇里发现的那个问题。&lt;/p&gt;

&lt;p&gt;最麻烦的地方在于：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;程序自己不知道。&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  7. 开启 trim：我们自己决定丢什么
&lt;/h1&gt;

&lt;p&gt;再跑一遍。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- round 6: messages=4 approx_tokens≈1372 ---
     trim: tokens 1372→37, dropped_msgs=1, tools_truncated=0
OK  reply='1'
     usage: prompt=93 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这次就不一样了。&lt;/p&gt;

&lt;p&gt;trim 之后：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages=4
approx_tokens≈37
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt_tokens=93
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也明显降了下来。&lt;/p&gt;

&lt;p&gt;更重要的是：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;现在 messages 里剩下的内容，就是我们主动决定留下来的内容。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;最后模型回答：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这里要注意一个容易误解的地方。&lt;/p&gt;

&lt;p&gt;它并不意味着：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“模型终于找回了完整历史，所以正确回答了 1。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不是。&lt;/p&gt;

&lt;p&gt;完整的 12 轮历史本来就塞不进这个窗口。&lt;/p&gt;

&lt;p&gt;所以两种模式其实都可能相对于“完整历史”答错。&lt;/p&gt;

&lt;p&gt;真正的区别是：&lt;/p&gt;

&lt;h3&gt;
  
  
  不做 trim
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;程序里的 messages 很长
        ↓
Ollama 自己截
        ↓
模型看到什么，我们不完全知道
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  做 trim
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;程序里的 messages
        ↓
我们主动删掉旧内容
        ↓
剩下什么，模型就看到什么
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以这里追求的不是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“永远正确。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;而是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;至少让我知道模型现在到底看到了什么。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这对调试 Agent 非常重要。&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Calculator Agent 本身为什么不太容易触发？
&lt;/h1&gt;

&lt;p&gt;回到前面一直写的 Calculator Agent。&lt;/p&gt;

&lt;p&gt;它的上下文其实非常短：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user
assistant(tool_call)
tool
assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以默认：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONTEXT_MAX_TOKENS=8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;通常不会触发滑窗。&lt;/p&gt;

&lt;p&gt;也就是说，平时跑：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"123 * 456 + 789 是多少？"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;大概率还是正常的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user
assistant
tool
assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但如果开启：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--trace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后连续聊很多轮，情况就不一样了。&lt;/p&gt;

&lt;p&gt;这也是为什么 Context 治理不能只针对 Calculator。&lt;/p&gt;

&lt;p&gt;现在 Calculator 很短，不代表以后所有 Agent 都很短。&lt;/p&gt;

&lt;p&gt;一旦加入：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;多轮对话&lt;/li&gt;
&lt;li&gt;搜索&lt;/li&gt;
&lt;li&gt;fetch&lt;/li&gt;
&lt;li&gt;文件读取&lt;/li&gt;
&lt;li&gt;数据库查询&lt;/li&gt;
&lt;li&gt;大段 Tool 返回结果&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Context 很快就会成为问题。&lt;/p&gt;




&lt;h1&gt;
  
  
  9. trim 日志也要打出来
&lt;/h1&gt;

&lt;p&gt;为了方便观察，我在日志里增加了类似：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [joyjoy] context trim: tokens 9000→7800, dropped_msgs=3, tools_truncated=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这样至少能知道：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;原来：9000
现在：7800

删了多少：
3 条 message

Tool 有没有被截：
0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--trace&lt;/code&gt; 里也增加：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;approx_tokens≈...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这样就可以把自己的估算值和：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;usage.prompt_tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;放在一起看。&lt;/p&gt;

&lt;p&gt;这比只看：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent 最后回答了什么
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;有用得多。&lt;/p&gt;

&lt;p&gt;因为 Agent 出问题的时候，答案只是结果。&lt;/p&gt;

&lt;p&gt;真正需要知道的是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;这一轮模型到底拿到了什么上下文？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  10. 一个目前还没解决的问题：Tool Call 配对
&lt;/h1&gt;

&lt;p&gt;这里还有一个坑。&lt;/p&gt;

&lt;p&gt;现在为了实现简单，我是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;按 message 一条一条删除最旧消息。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;但 Agent 的 Tool Calling 有一个天然的配对关系。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assistant
tool_calls: calculator(...)

tool
9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这两个实际上属于同一个 turn。&lt;/p&gt;

&lt;p&gt;如果简单地按 message 删除，极端情况下可能出现：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool
9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;还在，&lt;/p&gt;

&lt;p&gt;但是对应的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assistant
tool_calls: calculator(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;已经被删了。&lt;/p&gt;

&lt;p&gt;这就变成了一个“孤儿 tool”。&lt;/p&gt;

&lt;p&gt;所以当前实现适合这个系列的最小 Agent，但如果继续往生产方向做，下一步应该改成：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;按 turn 删除，而不是按 message 删除。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;也就是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assistant + tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;作为一个整体处理。&lt;/p&gt;

&lt;p&gt;这个问题我先留在这里，后面继续扩展 Agent Loop 的时候再处理。&lt;/p&gt;




&lt;h1&gt;
  
  
  11. 那 Ollama 的 &lt;code&gt;num_ctx&lt;/code&gt; 和我们的 trim 到底谁说了算？
&lt;/h1&gt;

&lt;p&gt;其实是两层。&lt;/p&gt;

&lt;p&gt;第一层是我们自己的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;prepare_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;它决定：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;到底把哪些 messages 发给 API。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;第二层是 Ollama：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;num_ctx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;它决定：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;模型最终能处理多大的上下文。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以即使我们自己做了 trim，也不能认为：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepare_messages()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;之后就绝对安全。&lt;/p&gt;

&lt;p&gt;如果我们自己控制在：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但 Ollama：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;num_ctx=2048
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;那模型侧还是可能继续截。&lt;/p&gt;

&lt;p&gt;所以完整链路其实是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;原始 messages
      ↓
Tool 内容截断
      ↓
滑动窗口
      ↓
prepare_messages()
      ↓
发送给 Ollama
      ↓
num_ctx 再做最终容量限制
      ↓
模型
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这也是为什么压测的时候，我会故意把：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;num_ctx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;调得很小。&lt;/p&gt;

&lt;p&gt;这样可以更容易把问题暴露出来。&lt;/p&gt;




&lt;h2&gt;
  
  
  12. 可选关闭 trim 与前三篇串起来
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; &lt;code&gt;CONTEXT_TRIM_ENABLED=false&lt;/code&gt; 会回到第二篇行为 — messages 堆长、Ollama 可能静默截断。trim 不是神秘组件，只是 &lt;strong&gt;Loop 里的 Context Governance&lt;/strong&gt;，与具体 Tool 无关。&lt;/p&gt;

&lt;p&gt;前三篇串线：Tool+Loop → &lt;code&gt;messages&lt;/code&gt; 短期记忆 → &lt;strong&gt;&lt;code&gt;prepare_messages&lt;/code&gt;（tool 截断 + 滑窗）&lt;/strong&gt; → 再进 LLM。压测要对比 &lt;strong&gt;无 trim / 有 trim&lt;/strong&gt; 的 &lt;code&gt;prompt_tokens&lt;/code&gt; 与模型是否还能看到开头，而不是只写一句「需要滑窗」。&lt;/p&gt;




&lt;h2&gt;
  
  
  13. 收束
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Context 不够时 &lt;strong&gt;别等报错&lt;/strong&gt; — 可能静默截断。&lt;/li&gt;
&lt;li&gt;最小组合：&lt;strong&gt;Tool 返回截断&lt;/strong&gt; + &lt;strong&gt;滑窗删旧消息&lt;/strong&gt;；在每次 &lt;code&gt;chat()&lt;/code&gt; 前 &lt;code&gt;prepare_messages(state.messages)&lt;/code&gt;，让 trace 与模型所见一致。&lt;/li&gt;
&lt;li&gt;压测同时看：&lt;code&gt;messages&lt;/code&gt; 条数、估算 token、&lt;code&gt;prompt_tokens&lt;/code&gt;、trim 日志、最终答案。&lt;/li&gt;
&lt;li&gt;按条删 message 可能拆散 &lt;code&gt;tool_calls&lt;/code&gt; 与 &lt;code&gt;tool&lt;/code&gt; 配对 — 后续可改按 turn 删、摘要压缩、更准 token 计数。&lt;/li&gt;
&lt;li&gt;联网 &lt;code&gt;fetch&lt;/code&gt; 后，&lt;strong&gt;Tool 内截断 + Loop trim&lt;/strong&gt; 两层会一起起作用（第四篇起更明显）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;代码：&lt;a href="https://github.com/zealot2002/myagent" rel="noopener noreferrer"&gt;zealot2002/myagent&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  下一篇
&lt;/h2&gt;

&lt;p&gt;《&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/04-%E5%A4%9ATool%E4%B8%8E%E8%81%94%E7%BD%91%E6%A3%80%E7%B4%A2.md" rel="noopener noreferrer"&gt;从零手写 Agent（四）：多 Tool 与联网检索——Loop 还是那个 Loop&lt;/a&gt;》——注册 &lt;code&gt;search_web&lt;/code&gt;、&lt;code&gt;fetch_url&lt;/code&gt;，扩展能力不改 Loop 形状。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>agents</category>
      <category>contextwindow</category>
      <category>trim</category>
      <category>ollama</category>
    </item>
    <item>
      <title>从零手写 Agent（二）：短期记忆，其实就是你一直带着的 messages</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:05:08 +0000</pubDate>
      <link>https://dev.to/zealot2002/cong-ling-shou-xie-agenter-duan-qi-ji-yi-qi-shi-jiu-shi-ni-zhi-dai-zhao-de-messages-4552</link>
      <guid>https://dev.to/zealot2002/cong-ling-shou-xie-agenter-duan-qi-ji-yi-qi-shi-jiu-shi-ni-zhi-dai-zhao-de-messages-4552</guid>
      <description>&lt;h1&gt;
  
  
  从零手写 Agent（二）：短期记忆，其实就是你一直带着的 messages
&lt;/h1&gt;

&lt;p&gt;上一篇把 Tool 跑通以后，我开始给 Agent 加多轮对话。&lt;/p&gt;

&lt;p&gt;然后很快就踩了一个特别容易踩的坑。&lt;/p&gt;

&lt;p&gt;我先运行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"3*3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;程序正常算出了 9。&lt;/p&gt;

&lt;p&gt;接着，我又开了一个命令：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"刚才我问了什么问题？"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;结果模型当然答不上来。&lt;/p&gt;

&lt;p&gt;当时第一反应是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Agent 怎么连上一句话都记不住？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;后来顺着 &lt;code&gt;messages&lt;/code&gt; 和 trace 一路看下去，我才发现，这个问题其实一点都不神秘。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;所谓短期记忆，本质上就是：下一次请求的时候，你有没有把之前的 &lt;code&gt;messages&lt;/code&gt; 一起带给模型。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;没有什么隐藏的 Memory 模块。&lt;/p&gt;

&lt;p&gt;也没有模型偷偷替你保存上一轮聊天记录。&lt;/p&gt;

&lt;p&gt;你把历史放进上下文，它就能看到；你没放，它就真的不知道。&lt;/p&gt;

&lt;p&gt;这件事听起来简单，但它直接牵扯到 Agent 里一个非常核心的问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;上下文窗口到底是什么？为什么 Agent 聊得越久越容易出问题？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这篇就从我踩的这个坑开始，把这几个问题捋清楚。&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 我之前理解的“记忆”，其实有点想多了
&lt;/h2&gt;

&lt;p&gt;先看最简单的 Chat Completions 流程。&lt;/p&gt;

&lt;p&gt;每次调用 LLM，大概都是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages
   ↓
LLM
   ↓
assistant / tool_calls
   ↓
append 到 messages
   ↓
下一次继续发送整个 messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;代码层面其实就是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;下一轮：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;还是把当前的 &lt;code&gt;messages&lt;/code&gt; 整体传进去。&lt;/p&gt;

&lt;p&gt;所以我现在比较愿意用一句话来理解：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;短期记忆 = 当前还留在 &lt;code&gt;messages&lt;/code&gt; 里的历史。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;比如一次 Calculator Agent：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0] system
[1] user: 3*3
[2] assistant: tool_calls → calculator
[3] tool: 9
[4] assistant: 3*3 的结果是 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;下一轮用户再问：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;刚才我问了什么问题？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;只要新的 user message 继续 append 到这个列表里：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0] system
[1] user: 3*3
[2] assistant: tool_calls
[3] tool: 9
[4] assistant: 3*3 的结果是 9
[5] user: 刚才我问了什么问题？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型就能看到之前发生过什么。&lt;/p&gt;

&lt;p&gt;所以 Agent 里的 &lt;code&gt;State&lt;/code&gt;，在我这个最小实现里，核心其实就是这个：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;我额外写的 &lt;code&gt;AgentState&lt;/code&gt;，更多只是给这个列表套一层壳，再放一些调试信息，比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;steps
tool_results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;真正给模型看的，还是 &lt;code&gt;messages&lt;/code&gt;。&lt;/p&gt;




&lt;h2&gt;
  
  
  2. 那为什么我刚才换一个命令，模型就“失忆”了？
&lt;/h2&gt;

&lt;p&gt;现在回头看就很容易解释了。&lt;/p&gt;

&lt;p&gt;我之前运行的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"3*3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这个进程结束以后，里面的 &lt;code&gt;AgentState&lt;/code&gt; 也没了。&lt;/p&gt;

&lt;p&gt;然后我又运行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"刚才我问了什么问题？"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这已经是一个&lt;strong&gt;全新的进程&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;它拿到的 messages 只有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user: 刚才我问了什么问题？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;根本没有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user: 3*3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型当然不知道。&lt;/p&gt;

&lt;p&gt;所以这里有个特别重要的区别：&lt;/p&gt;

&lt;h3&gt;
  
  
  同一个进程
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;你&amp;gt; 3*3
你&amp;gt; 刚才我问了什么问题？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第二句话可以看到第一句话。&lt;/p&gt;

&lt;p&gt;因为两次输入都 append 到了同一个 &lt;code&gt;messages&lt;/code&gt;。&lt;/p&gt;

&lt;h3&gt;
  
  
  两个独立进程
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"3*3"&lt;/span&gt;

python main.py &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"刚才我问了什么问题？"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第二个进程没有第一条消息。&lt;/p&gt;

&lt;p&gt;自然也就不存在所谓的“刚才”。&lt;/p&gt;

&lt;p&gt;所以：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;多轮对话不是模型自己记住了，而是程序一直把历史带着。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这一点我觉得是理解 Agent Memory 最重要的第一步。&lt;/p&gt;




&lt;h2&gt;
  
  
  3. 那上下文窗口又是什么？
&lt;/h2&gt;

&lt;p&gt;搞明白 &lt;code&gt;messages&lt;/code&gt; 之后，下一个问题马上就来了：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;如果一直 append，不就可以一直记了吗？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;当然不行。&lt;/p&gt;

&lt;p&gt;因为模型一次能处理的 token 是有限的。&lt;/p&gt;

&lt;p&gt;这个限制，就是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;上下文窗口（Context Window）&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我现在比较喜欢把它理解成：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;模型一次请求最多能装多少 token 的工作区。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;而 &lt;code&gt;messages&lt;/code&gt; 就是往这个工作区里不断塞东西。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user
assistant
tool
assistant
user
assistant
tool
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这些内容都要占上下文。&lt;/p&gt;

&lt;p&gt;而且别忘了，Tool 也会占。&lt;/p&gt;

&lt;p&gt;比如一个简单 Calculator Agent：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user
assistant + tool_calls
tool
assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;看起来没多少内容。&lt;/p&gt;

&lt;p&gt;但如果以后把 Tool 换成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch_web_page()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;返回一整页网页：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool
└── 几万字网页正文
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;那情况就完全不一样了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;消息条数可能没增加多少，但 token 一下就上去了。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;所以 Agent 比普通 Chat 更容易碰到上下文问题。&lt;/p&gt;




&lt;h2&gt;
  
  
  4. 上下文窗口和 MAX_STEPS，不是一回事
&lt;/h2&gt;

&lt;p&gt;这里我也踩过一个容易混淆的概念。&lt;/p&gt;

&lt;p&gt;我的 Agent 里有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MAX_STEPS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;MAX_STEPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;它表示：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;一次任务最多调用多少次 LLM。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;而上下文窗口表示：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;每一次调用 LLM，最多能塞多少 token。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;两个完全不是一个维度。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAX_STEPS = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也可能出问题。&lt;/p&gt;

&lt;p&gt;因为第一轮 Tool 返回了 10 万字。&lt;/p&gt;

&lt;p&gt;虽然只跑了两步，但第二次请求已经可能装不下。&lt;/p&gt;

&lt;p&gt;反过来也一样。&lt;/p&gt;

&lt;p&gt;你可以：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAX_STEPS = 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但每一步都很短，整个上下文也可能还没到上限。&lt;/p&gt;

&lt;p&gt;所以可以简单记成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MAX_STEPS
    ↓
最多跑多少步

Context Window
    ↓
每一步最多能装多少 token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这两个东西后面都会影响 Agent，但解决的是不同的问题。&lt;/p&gt;




&lt;h2&gt;
  
  
  5. 我打开 trace 以后，终于看清楚“记忆”到底是什么
&lt;/h2&gt;

&lt;p&gt;比如执行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py &lt;span class="nt"&gt;--trace&lt;/span&gt; &lt;span class="s2"&gt;"3*3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;典型的 messages：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;下标&lt;/th&gt;
&lt;th&gt;role&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;th&gt;占上下文吗&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[0]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;system&lt;/td&gt;
&lt;td&gt;系统规则、时间&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3*3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assistant&lt;/td&gt;
&lt;td&gt;tool_calls&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[3]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[4]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;assistant&lt;/td&gt;
&lt;td&gt;最终答案&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;这一张表其实就把很多问题说明白了。&lt;/p&gt;

&lt;p&gt;比如第一篇讲的 Tool。&lt;/p&gt;

&lt;p&gt;Tool 调完以后，结果并不是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“计算完成，然后从 Agent 里消失。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;而是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[3] tool: 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这条消息还留在 &lt;code&gt;messages&lt;/code&gt; 里。&lt;/p&gt;

&lt;p&gt;所以第二次调用 LLM 时：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0]
[1]
[2]
[3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这些历史仍然会一起送进去。&lt;/p&gt;

&lt;p&gt;模型看到：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user: 3*3
assistant: tool_calls
tool: 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;它自然就知道刚才发生了什么。&lt;/p&gt;




&lt;h2&gt;
  
  
  6. 一个容易误解的地方：tool_results 是不是记忆？
&lt;/h2&gt;

&lt;p&gt;我的 &lt;code&gt;AgentState&lt;/code&gt; 里还有一个：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tool_results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;里面也会保存：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculator → 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;刚开始我也会觉得：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;那模型是不是同时从 &lt;code&gt;tool_results&lt;/code&gt; 里读取结果？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不是。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tool_results&lt;/code&gt; 只是我自己为了调试和记录留的一份副本。&lt;/p&gt;

&lt;p&gt;模型真正看到的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;role: tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;对应的 message。&lt;/p&gt;

&lt;p&gt;所以：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AgentState.tool_results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;和：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages[].role == "tool"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;不是一回事。&lt;/p&gt;

&lt;p&gt;前者主要是给程序和人看的。&lt;/p&gt;

&lt;p&gt;后者才是模型上下文的一部分。&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Agent 为什么比普通 Chat 更吃上下文？
&lt;/h2&gt;

&lt;p&gt;这个问题到这里就比较好理解了。&lt;/p&gt;

&lt;p&gt;普通 Chat 可能就是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user
assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而 Agent 一次问题可能变成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user
assistant + tool_calls
tool
assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;如果一次任务里连续调用多个 Tool：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;system
user
assistant + tool_calls
tool
assistant + tool_calls
tool
assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;消息越来越长。&lt;/p&gt;

&lt;p&gt;更麻烦的是，Tool 返回的内容有时候也非常长。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;返回一大段网页。&lt;/p&gt;

&lt;p&gt;或者：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read_file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;把几万字日志直接塞进：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;role: tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;那上下文很快就会被吃掉。&lt;/p&gt;

&lt;p&gt;所以我现在看 Agent 的上下文问题，会特别注意三个东西：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;背后的原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;对话越来越慢、越来越贵&lt;/td&gt;
&lt;td&gt;每次请求都要重新带历史&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool 返回特别长&lt;/td&gt;
&lt;td&gt;Observation 占掉大量上下文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent 跑很多步&lt;/td&gt;
&lt;td&gt;每一步都会留下新的 message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;也就是说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent 的每一步，不只是增加一次调用，也是在给下一次请求增加上下文负担。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  8. “每轮都把 messages 全发一遍”，是不是很浪费？
&lt;/h2&gt;

&lt;p&gt;是。&lt;/p&gt;

&lt;p&gt;但这是当前这种实现方式本身决定的。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;第一次：
system + user

第二次：
system + user + assistant + tool

第三次：
system + user + assistant + tool + assistant + user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;每次请求都会带上当前完整历史。&lt;/p&gt;

&lt;p&gt;所以第二次并不是只告诉模型：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;刚才 Tool 返回了 9。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而是把整个上下文重新带过去。&lt;/p&gt;

&lt;p&gt;这也是为什么多轮 Agent 的 token 成本会不断增加。&lt;/p&gt;

&lt;p&gt;当然，具体怎么计费要看平台。&lt;/p&gt;

&lt;p&gt;但从实现角度看，有一点是确定的：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;你让模型每一轮重新看到完整历史，就意味着每一轮都要重新处理这些历史。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这不是 Loop 写错了。&lt;/p&gt;

&lt;p&gt;而是这种上下文工作方式本身就是如此。&lt;/p&gt;




&lt;h2&gt;
  
  
  9. 那上下文到底能有多长？
&lt;/h2&gt;

&lt;p&gt;这个问题很容易被各种模型宣传页搞得特别复杂。&lt;/p&gt;

&lt;p&gt;因为不同模型、不同版本、不同 endpoint，窗口都可能不一样。&lt;/p&gt;

&lt;p&gt;所以我这里不想把某个数字当成永久真理。&lt;/p&gt;

&lt;p&gt;查模型规格时，最好还是以：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;官方文档
+
你实际使用的 endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;为准。&lt;/p&gt;

&lt;p&gt;我当时本地跑的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;qwen2.5:3b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;通过：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama show qwen2.5:3b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;看到本机配置的 context length 是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;32768
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而且还有一个容易忽略的问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;模型本身支持多大的 context，和你实际运行时给它多大的 context，不一定是一回事。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ollama 运行时还会受到：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;num_ctx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;等配置影响。&lt;/p&gt;

&lt;p&gt;所以不要看到模型卡上写了一个数字，就默认自己的 Agent 一定能用满。&lt;/p&gt;




&lt;h2&gt;
  
  
  10. 我专门做了一次压力测试
&lt;/h2&gt;

&lt;p&gt;理论说到这里还不够。&lt;/p&gt;

&lt;p&gt;于是我专门写了一个脚本，故意把上下文窗口调得特别小：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/simulate_context_window.py &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--num-ctx&lt;/span&gt; 256 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--chunk-chars&lt;/span&gt; 8000 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--max-rounds&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;目的很简单：&lt;/p&gt;

&lt;p&gt;每一轮往 messages 里塞一大块文本，然后继续调用 LLM。&lt;/p&gt;

&lt;p&gt;看看上下文爆掉以后，到底会发生什么。&lt;/p&gt;

&lt;p&gt;正常情况下，我们可能会以为：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context 超了
↓
API 报错
↓
程序知道出问题了
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但本地 Ollama 的实际表现有点不一样。&lt;/p&gt;

&lt;p&gt;我当时看到的日志类似：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- round 1: messages=2 approx_input_tokens≈2677 (limit num_ctx=256) ---
OK  usage: prompt=2050 completion=301 total=2351

--- round 5: messages=10 approx_input_tokens≈14192 (limit num_ctx=256) ---
OK  usage: prompt=2050 completion=486 total=2536
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最让我注意的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;明明越来越长。&lt;/p&gt;

&lt;p&gt;但：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt_tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;却差不多卡在一个范围。&lt;/p&gt;

&lt;p&gt;也就是说：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;程序里的历史还在增长，但真正进入模型的上下文已经被截断了。&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  11. 这比直接报错还麻烦
&lt;/h2&gt;

&lt;p&gt;如果 API 直接告诉你：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context length exceeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;其实很好办。&lt;/p&gt;

&lt;p&gt;你看到错误就知道：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;上下文满了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;但静默截断就麻烦了。&lt;/p&gt;

&lt;p&gt;因为程序可能还是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;甚至正常返回一个答案。&lt;/p&gt;

&lt;p&gt;只是模型已经看不到最早那些内容了。&lt;/p&gt;

&lt;p&gt;于是就可能出现这种情况：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;程序里的 messages：

[1] 很早之前的重要信息
[2]
[3]
[4]
...
[10] 最近的信息
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;你打开 trace 一看：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;历史不是都在吗？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;但模型真正看到的可能只有：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[7]
[8]
[9]
[10]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;于是最早的信息就像“失忆”了一样。&lt;/p&gt;

&lt;p&gt;更麻烦的是，模型通常不会告诉你：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“抱歉，前面的上下文被截掉了。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;它可能直接根据自己现在看到的内容继续回答。&lt;/p&gt;

&lt;p&gt;这就是我觉得上下文超长特别麻烦的地方：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;程序层面看起来还正常，模型层面其实已经不是你以为的那个上下文了。&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  12. 所以，短期记忆其实是有保质期的
&lt;/h2&gt;

&lt;p&gt;到这里，我对“短期记忆”这个词的理解也发生了一点变化。&lt;/p&gt;

&lt;p&gt;它并不是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;模型记住了所有聊天内容
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而更接近：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;当前 messages
    ↓
能放进 context window
    ↓
模型才能看到
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以真正的链路是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;历史消息
   ↓
messages
   ↓
上下文窗口
   ↓
模型
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;任何一层出了问题，所谓“记忆”都会出问题。&lt;/p&gt;

&lt;p&gt;比如：&lt;/p&gt;

&lt;h3&gt;
  
  
  历史根本没保存
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;换了一个进程
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型当然不知道。&lt;/p&gt;

&lt;h3&gt;
  
  
  历史保存了，但太长
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;context window 不够
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型可能看不到前面的内容。&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool 返回太长
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Observation 吃掉大量窗口
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;早期对话就更容易被挤出去。&lt;/p&gt;

&lt;p&gt;所以：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“模型记不记得”很多时候不是模型有没有 Memory，而是你到底给了它多少上下文。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  13. 长期记忆、治理方向、Tool 占窗口（合并说清）
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;短期记忆&lt;/strong&gt; = 仍在 &lt;code&gt;messages&lt;/code&gt; 里、每轮请求都会带上 → 受 Context Window 限制。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;长期记忆&lt;/strong&gt; = 文件 / DB / 向量库；用时检索再 &lt;strong&gt;插入 messages&lt;/strong&gt;。RAG 不是绕过窗口，而是 &lt;strong&gt;先筛再塞&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;messages 不能无限 append&lt;/strong&gt; — 满了就要治理。常见手段：滑窗、摘要、Tool 结果截断、按需检索；第三篇先落地 &lt;strong&gt;截断 + 滑窗&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;assistant(tool_calls)&lt;/code&gt; 与 &lt;code&gt;tool&lt;/code&gt; 结果都会留在上下文&lt;/strong&gt; — Calculator 返回 &lt;code&gt;9&lt;/code&gt; 几乎无感，整页 &lt;code&gt;fetch_url&lt;/code&gt; 则会迅速吃满窗口（第四篇更明显）。&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  14. 收束
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;短期记忆 ≈ &lt;code&gt;messages&lt;/code&gt;&lt;/strong&gt;；「记得上一句」是因为下次请求仍带上它们。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;多轮&lt;/strong&gt; 要同进程 State（如 &lt;code&gt;python main.py -i&lt;/code&gt;）；两次独立命令是两次冷启动。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Window&lt;/strong&gt; 是模型一次能看的 token 上限，不是 &lt;code&gt;messages&lt;/code&gt; 列表长度。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent 更吃 token&lt;/strong&gt;：&lt;code&gt;tool_calls&lt;/code&gt; + 长 tool 正文会快速占满窗口。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama 可能静默截断&lt;/strong&gt; — &lt;code&gt;messages&lt;/code&gt; 还在变长，进模型的 prompt 已丢开头；用 &lt;code&gt;--trace&lt;/code&gt; 和 &lt;code&gt;prompt_tokens&lt;/code&gt; 对照，不要只看最终答案。&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  下一篇
&lt;/h2&gt;

&lt;p&gt;《&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/03-%E4%B8%8A%E4%B8%8B%E6%96%87%E8%B6%85%E9%95%BF%E6%80%8E%E4%B9%88%E5%8A%9E.md" rel="noopener noreferrer"&gt;从零手写 Agent（三）：上下文超长，别等模型替你截断&lt;/a&gt;》——在 Loop 里加 &lt;code&gt;prepare_messages()&lt;/code&gt;，用同一压测脚本对比「不处理 vs 主动 trim」。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>agents</category>
      <category>contextwindow</category>
      <category>ollama</category>
      <category>messages</category>
    </item>
    <item>
      <title>从零手写 Agent（一）：Tool 把我从 Chat 拽进了 Loop</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Thu, 22 Oct 2026 09:04:59 +0000</pubDate>
      <link>https://dev.to/zealot2002/cong-ling-shou-xie-agent-tool-ba-wo-cong-chat-zhuai-jin-liao-loop-34hc</link>
      <guid>https://dev.to/zealot2002/cong-ling-shou-xie-agent-tool-ba-wo-cong-chat-zhuai-jin-liao-loop-34hc</guid>
      <description>&lt;h1&gt;
  
  
  从零手写 Agent（一）：Tool 把我从 Chat 拽进了 Loop
&lt;/h1&gt;

&lt;p&gt;最近开始自己折腾 Agent。&lt;/p&gt;

&lt;p&gt;一开始我也没急着上 LangGraph 之类的框架，而是在本地用 Ollama 写了一个最小版本：一个 &lt;code&gt;calculator&lt;/code&gt;，一个 &lt;code&gt;while&lt;/code&gt; 循环，再加上 OpenAI 兼容的 Tool Calling。&lt;/p&gt;

&lt;p&gt;代码其实没多少，但跑通以后，我反而觉得这件事比看框架源码有意思。&lt;/p&gt;

&lt;p&gt;因为真正把 Calculator Agent 跑起来之后，我才发现：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent 和普通 Chat 的区别，并不是 Prompt 写得更长，而是模型开始参与“决定下一步做什么”。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;而 Tool，就是这个变化真正发生的地方。&lt;/p&gt;

&lt;p&gt;这篇不讲框架，也不打算把 Agent 的概念从头到尾讲一遍。主要记录写这个最小 Agent 时遇到的几个问题，以及跑日志以后对 Tool、Loop、State 的一些理解。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;) · 代码见文末仓库链接。&lt;/p&gt;




&lt;h2&gt;
  
  
  1. 一开始，我以为 Agent 就是 Prompt 写得更复杂
&lt;/h2&gt;

&lt;p&gt;普通 Chat 很简单：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → LLM → Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以我最开始对 Agent 的理解也很朴素：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;不就是在 Prompt 里告诉模型：“你可以使用这些工具”吗？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;真自己跑起来以后，发现完全不是这么回事。&lt;/p&gt;

&lt;p&gt;一个最简单的 Agent，实际执行过程更像这样：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
  ↓
LLM
  ↓
需要工具？
  ├─ 否 → Answer
  │
  └─ 是
       ↓
    Tool Call
       ↓
    执行 Tool
       ↓
    Observation
       ↓
    LLM
       ↓
    Answer / 再调 Tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;换成代码，其实就是一个非常普通的循环：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_STEPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_tool_call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;assistant_turn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assistant_turn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_result_turn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;到这里，我才真正理解了之前经常看到的那句话：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent ≈ LLM + Tools + Loop + State&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;以前看这句话的时候，感觉多少有点像概念总结。&lt;/p&gt;

&lt;p&gt;自己写一遍以后就不一样了。&lt;/p&gt;

&lt;p&gt;所谓 Loop，本质就是：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;模型说下一步想干什么 → 程序真的去干 → 把结果塞回来 → 再让模型决定下一步。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这才是 Agent 开始“动起来”的地方。&lt;/p&gt;




&lt;h2&gt;
  
  
  2. 那 Tool 到底是什么？
&lt;/h2&gt;

&lt;p&gt;这个问题我一开始也容易和 Function Call 混在一起。&lt;/p&gt;

&lt;p&gt;后来我觉得可以用一句比较简单的话理解：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tool = 给模型看的说明书 + 程序里真正执行的函数。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这里其实有两个角色。&lt;/p&gt;

&lt;p&gt;模型负责：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;要不要调用 Tool&lt;/li&gt;
&lt;li&gt;调哪个 Tool&lt;/li&gt;
&lt;li&gt;参数应该怎么填&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;程序负责：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;接收模型给出的参数&lt;/li&gt;
&lt;li&gt;真正执行函数&lt;/li&gt;
&lt;li&gt;把执行结果返回给模型&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;比如我们有一个：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型可能会输出：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123 * 456 + 789"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;注意，这时候&lt;strong&gt;计算还没有发生&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;模型只是说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“我想调用 calculator，而且参数是这个。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;真正执行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123 * 456 + 789&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;的是我的 Python 程序。&lt;/p&gt;

&lt;p&gt;所以 Tool Calling 这件事，可以简单理解成：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
 ↓
“我要调用 calculator，参数是 xxx”
 ↓
你的程序
 ↓
真的执行 calculator
 ↓
返回结果
 ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这点很重要。&lt;/p&gt;

&lt;p&gt;因为模型发出来的 Tool Call，本质上只是一个&lt;strong&gt;请求或者提议&lt;/strong&gt;，不是程序真的执行了什么。&lt;/p&gt;

&lt;p&gt;参数也不一定永远正确。&lt;/p&gt;

&lt;p&gt;所以 Agent Loop 不能把模型输出当成绝对可靠的函数调用，必须有一层真正的执行逻辑接住它。&lt;/p&gt;




&lt;h2&gt;
  
  
  3. 第一个 Calculator Agent 跑起来以后，我才知道 Loop 在干什么
&lt;/h2&gt;

&lt;p&gt;我先问一个最简单的问题：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;123 * 456 + 789 是多少？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型用的是 &lt;code&gt;qwen2.5:3b&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;实际日志：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [joyjoy] agent step 1/10
INFO [joyjoy] tool call: calculator({'expression': '123 * 456 + 789'})
INFO [joyjoy] tool result: 56877
INFO [joyjoy] agent step 2/10

计算结果是 56877。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第一次看到这个日志，我觉得挺有意思。&lt;/p&gt;

&lt;p&gt;因为它其实把 Agent 的工作过程完整暴露出来了。&lt;/p&gt;

&lt;p&gt;第一步：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
↓
LLM
↓
calculator("123 * 456 + 789")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第二步：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculator
↓
56877
↓
LLM
↓
“计算结果是 56877”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以这里出现两个容易误解的地方。&lt;/p&gt;

&lt;h3&gt;
  
  
  第一，为什么要跑两步？
&lt;/h3&gt;

&lt;p&gt;因为第一轮模型的任务不是“说人话”，而是决定：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这个问题需要调用 calculator。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;于是第一轮主要产生的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool_call
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;工具执行以后，模型拿到 &lt;code&gt;56877&lt;/code&gt;，第二轮才负责组织最终回答。&lt;/p&gt;

&lt;p&gt;所以：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;两步不是多余的，而是 Tool Calling 的正常流程。&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  第二，真正完成计算的是谁？
&lt;/h3&gt;

&lt;p&gt;不是 LLM。&lt;/p&gt;

&lt;p&gt;是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LLM 做的是理解问题，然后决定：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculator("123 * 456 + 789")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;程序负责真正计算。&lt;/p&gt;

&lt;p&gt;这也是我觉得 Tool 很有价值的地方：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;让模型负责“理解”，让程序负责“确定性”。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;计算器只是一个很简单的例子。&lt;/p&gt;

&lt;p&gt;如果换成搜索、数据库、文件系统、API，逻辑其实还是一样。&lt;/p&gt;




&lt;h2&gt;
  
  
  4. 模型到底怎么知道“现在应该调 Tool”？
&lt;/h2&gt;

&lt;p&gt;这个问题我刚开始也想得比较简单。&lt;/p&gt;

&lt;p&gt;我以为背后是不是有一个东西：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;用户问题
↓
关键词检测
↓
出现“计算”
↓
调用 calculator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;实际上不是。&lt;/p&gt;

&lt;p&gt;模型看到的是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;system prompt&lt;/li&gt;
&lt;li&gt;用户问题&lt;/li&gt;
&lt;li&gt;历史 messages&lt;/li&gt;
&lt;li&gt;Tool 的 description/schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后自己决定这一轮应该输出普通文本，还是 &lt;code&gt;tool_calls&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;123 * 456 + 789 是多少？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;它可能选择：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool_calls → calculator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;而：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent 是什么？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;大概率直接：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assistant → 普通文本
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这里有一个很容易低估的东西：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool description。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我现在越来越觉得，Tool description 有点像给模型看的 API 文档。&lt;/p&gt;

&lt;p&gt;如果你有几个功能非常相似的 Tool，description 写得含糊，模型很容易选错。&lt;/p&gt;

&lt;p&gt;所以 Tool 的名字固然重要，但真正影响模型选择的，往往是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tool 是干什么的
什么时候应该用
什么时候不要用
参数是什么
返回什么
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;尤其是小模型。&lt;/p&gt;

&lt;p&gt;我用的只是本地 &lt;code&gt;qwen2.5:3b&lt;/code&gt;，它并不是每次都能做出最合理的选择。&lt;/p&gt;

&lt;p&gt;所以实际调 Agent 的时候，我现在比较习惯先看 trace，而不是先怀疑 Loop。&lt;/p&gt;




&lt;h2&gt;
  
  
  5. 模型明明会算，为什么还要 Calculator？
&lt;/h2&gt;

&lt;p&gt;这个问题也挺有意思。&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;123 * 456 + 789
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型自己其实很可能也能算出来。&lt;/p&gt;

&lt;p&gt;那为什么还要 Tool？&lt;/p&gt;

&lt;p&gt;我的理解是：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;很多时候我们要的不是“模型觉得答案是多少”，而是一个可以验证、可以记录、可以复现的结果。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;计算尤其明显。&lt;/p&gt;

&lt;p&gt;让 LLM 负责：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;理解题目
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;让程序负责：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;计算
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;最后得到的结果就比较容易验证。&lt;/p&gt;

&lt;p&gt;同样的思路放到其他场景里也成立：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;查数据库 → 数据库负责事实
搜索网页 → 搜索服务负责事实
调用 API → API 返回真实状态
执行操作 → 程序负责副作用
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以 Tool 的意义并不是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“模型不会做，所以给它一个工具。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;更准确一点应该是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;模型适合做判断和规划，但某些事情交给确定性的程序更靠谱。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. 一个很有意思的坑：没有 Tool，Agent 就不会回答了吗？
&lt;/h2&gt;

&lt;p&gt;然后我遇到了一个挺有意思的问题。&lt;/p&gt;

&lt;p&gt;当时 Agent 里只有一个：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;我突然问：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;今天是几号？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;结果模型没调 Tool，而是直接说：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;请提供当前日期或使用可以获取当前日期的服务或工具。
我目前无法直接获取当前日期。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;当时第一反应是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Agent 是不是坏了？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;后来仔细看了一下，Loop 根本没问题。&lt;/p&gt;

&lt;p&gt;这一轮发生的是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM
↓
没有 tool_call
↓
返回普通文本
↓
Loop 结束
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;问题其实在另外一个地方：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent 根本没有获得“当前时间”这个能力。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这让我意识到一个很容易混淆的问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Agent 能不能完成任务，和 Loop 写得对不对，是两件事。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Loop 只负责：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;模型 → Tool → 模型 → Tool → ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;至于模型有没有能力完成当前任务，取决于你给它什么。&lt;/p&gt;




&lt;h2&gt;
  
  
  7. “今天”到底从哪里来？
&lt;/h2&gt;

&lt;p&gt;这个问题我后来又专门拆了一下。&lt;/p&gt;

&lt;p&gt;如果没有：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;当前时间&lt;/li&gt;
&lt;li&gt;时间 Tool&lt;/li&gt;
&lt;li&gt;其他环境信息&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;那就不能指望模型凭空知道“今天”。&lt;/p&gt;

&lt;p&gt;模型没有一块隐藏的硬件时钟。&lt;/p&gt;

&lt;p&gt;我现在这个最小 Agent 里，采用的是另一种简单办法：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;直接把当前时间写进 system prompt。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;【系统环境】当前本地时间：
2026-10-22 10:54:36 +0800
（日期以 YYYY-MM-DD 为准）
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以进入 Loop 时，messages 大概是：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;【系统环境】当前本地时间：2026-10-22 ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;今天是几号？&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后模型只是读取上下文里的这段文字。&lt;/p&gt;

&lt;p&gt;它并没有真的跑到我的电脑上读取系统时间。&lt;/p&gt;

&lt;p&gt;这一点和 Tool 有一个很直观的区别：&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;System 注入&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;信息在哪里&lt;/td&gt;
&lt;td&gt;&lt;code&gt;system.content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;role: tool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;是否需要额外 Loop&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;td&gt;通常需要&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;谁提供&lt;/td&gt;
&lt;td&gt;宿主程序&lt;/td&gt;
&lt;td&gt;Tool 执行结果&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;例子&lt;/td&gt;
&lt;td&gt;当前时间&lt;/td&gt;
&lt;td&gt;查询数据库&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;这个区分对我来说挺重要。&lt;/p&gt;

&lt;p&gt;因为以前容易产生一种误解：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“只要 Agent 有 Tool，所有外部信息都应该通过 Tool 获取。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;其实没必要。&lt;/p&gt;

&lt;p&gt;一些稳定、基础的环境信息，直接注入 system 就够了。&lt;/p&gt;

&lt;p&gt;而需要动态查询、执行或者验证的东西，再考虑 Tool。&lt;/p&gt;




&lt;h2&gt;
  
  
  8. 同一个问题，为什么有时候结果还不一样？
&lt;/h2&gt;

&lt;p&gt;把当前时间注入 system 以后，再问：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;今天几号？
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;情况就开始变得有意思了。&lt;/p&gt;

&lt;p&gt;理想情况下：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [joyjoy] agent step 1/10

2026-10-22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;一步结束。&lt;/p&gt;

&lt;p&gt;但小模型偶尔也会犯迷糊。&lt;/p&gt;

&lt;p&gt;比如出现：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [joyjoy] tool call:
calculator({'expression': 'today().day'})

WARNING [joyjoy] tool calculator failed:
unsupported expression: Attribute

INFO [joyjoy] tool result:
Error: unsupported expression: Attribute

INFO [joyjoy] agent step 2/10

今天是2026-10-22……
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;也就是说，它先误用了 calculator，发现不行以后，再根据 system 里的时间把答案答出来。&lt;/p&gt;

&lt;p&gt;甚至还有更极端的情况：&lt;/p&gt;

&lt;p&gt;模型在 &lt;code&gt;content&lt;/code&gt; 里自己输出了一段看起来像 JSON 的东西，但协议层面并没有真正产生 &lt;code&gt;tool_calls&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;这种情况下，Loop 只能把它当普通文本处理。&lt;/p&gt;

&lt;p&gt;这也让我对一个问题有了比较现实的认识：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent 不是写完 Loop 就万事大吉。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;实际效果还和下面几个东西强相关：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;模型
Tool description
System prompt
输出协议
上下文
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;所以调 Agent 的时候，我现在更愿意看真实 trace。&lt;/p&gt;

&lt;p&gt;不要光盯着最终答案。&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Tool 一多，问题就来了
&lt;/h2&gt;

&lt;p&gt;目前我的 Calculator Agent 只有一个 Tool，所以事情还很简单。&lt;/p&gt;

&lt;p&gt;但如果继续加：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calculator
search_web
read_file
query_database
get_current_datetime
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型就需要从一堆 Tool 里做选择。&lt;/p&gt;

&lt;p&gt;这时候我目前比较认可几个原则。&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool 的职责要尽量清楚
&lt;/h3&gt;

&lt;p&gt;比如：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search_web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;负责搜索网页。&lt;/p&gt;

&lt;p&gt;那就别再搞一个：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web_search
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;两者干一样的事情。&lt;/p&gt;

&lt;p&gt;对模型来说，这相当于：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“这里有两个看起来差不多的按钮，你自己猜按哪个。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  description 比名字更重要
&lt;/h3&gt;

&lt;p&gt;Tool 的 description 最好直接说清楚：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;负责什么
什么时候用
什么时候不要用
参数怎么传
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;把它当 API 文档写就行。&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool 不是越多越好
&lt;/h3&gt;

&lt;p&gt;协议层面可以注册很多 Tool。&lt;/p&gt;

&lt;p&gt;但真正的问题是：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;模型到底能不能稳定选对？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;如果一个 Agent 同时挂几十个功能，模型选择本身就开始变成问题。&lt;/p&gt;

&lt;p&gt;到了这个阶段，通常应该考虑 Router、多 Agent 或者 Tool 分组，而不是继续往一个 Agent 里塞。&lt;/p&gt;




&lt;h2&gt;
  
  
  10. 我现在调 Agent，基本就按这个顺序排查
&lt;/h2&gt;

&lt;p&gt;这一点是我写完 Calculator 之后最大的一个变化。&lt;/p&gt;

&lt;p&gt;以前遇到：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent 答错了
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;第一反应可能是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Loop 有 bug？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;现在我会先看 trace，然后把问题分成几类：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. 漏调 Tool
2. 误调 Tool
3. 没有合适 Tool
4. Tool 自己执行失败
5. 模型输出协议有问题
6. 上下文/Prompt 有问题
7. 真正的代码 bug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后一次只改一个地方，再重新跑。&lt;/p&gt;

&lt;p&gt;比如模型不调 calculator：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;先看 Tool description
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型乱调 calculator：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;看 description + system + 模型能力
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool 调对了，但执行失败：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;查 execute_tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;模型明明应该知道时间却不知道：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;查 system 环境信息
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;这样排查起来，比一上来修改 while 循环有效得多。&lt;/p&gt;




&lt;h2&gt;
  
  
  11. 还有一个和 Agent 完全无关的坑：代理
&lt;/h2&gt;

&lt;p&gt;最后记录一个比较无聊，但确实浪费我时间的问题。&lt;/p&gt;

&lt;p&gt;接 Ollama 的时候，OpenAI SDK 报：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;502 Bad Gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;但我直接：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:11434
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;却是正常的。&lt;/p&gt;

&lt;p&gt;最后发现不是 Agent，也不是 Tool，更不是 Ollama 挂了。&lt;/p&gt;

&lt;p&gt;是 macOS 的 HTTP 代理把 localhost 也转发出去了。&lt;/p&gt;

&lt;p&gt;把客户端的：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trust_env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;处理好之后才恢复正常。&lt;/p&gt;

&lt;p&gt;所以有时候 Agent 看起来“挂了”，真的不一定是 Agent 的问题。&lt;/p&gt;

&lt;p&gt;特别是本地模型这种环境，网络代理、endpoint、环境变量这些东西也得一起查。&lt;/p&gt;




&lt;h2&gt;
  
  
  12. 收束
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool = Schema + 你的实现&lt;/strong&gt;；执行永远在进程里，不在模型权重里。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent ≈ Loop&lt;/strong&gt;：LLM → 是否 &lt;code&gt;tool_calls&lt;/code&gt; → 执行 → 结果进 &lt;code&gt;messages&lt;/code&gt; → 再调 LLM。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;何时调 Tool&lt;/strong&gt; 由问题、历史、system、Tool description 和模型能力共同决定 — description 要写准。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;缺能力&lt;/strong&gt;（如当前日期）应加 Tool/上下文，而不是改 Loop 逻辑糊弄过去。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--trace&lt;/code&gt;&lt;/strong&gt; 比架构图直观：看清 &lt;code&gt;assistant(tool_calls)&lt;/code&gt; → &lt;code&gt;tool&lt;/code&gt; → &lt;code&gt;assistant&lt;/code&gt; 最终答。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;建议路径：先手写几十行 Loop + 一个 Calculator，故意跑坏几次，再去看框架。&lt;/p&gt;




&lt;h2&gt;
  
  
  下一篇
&lt;/h2&gt;

&lt;p&gt;《&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/02-%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AA%97%E5%8F%A3%E4%B8%8E%E5%A4%9A%E8%BD%AE%E5%AF%B9%E8%AF%9D.md" rel="noopener noreferrer"&gt;从零手写 Agent（二）：上下文窗口与多轮对话&lt;/a&gt;》——&lt;code&gt;messages&lt;/code&gt; 就是短期记忆；&lt;code&gt;-i&lt;/code&gt; 与同进程 State；Context Window 与 Ollama 静默截断。&lt;/p&gt;

&lt;p&gt;系列目录：&lt;a href="https://github.com/zealot2002/myagent/tree/main/docs" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;(&lt;a href="https://github.com/zealot2002/myagent/blob/main/docs/00-%E7%B3%BB%E5%88%97%E5%AF%BC%E8%AF%BB.md" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md&lt;/a&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  附录：几个关键日志
&lt;/h2&gt;

&lt;h3&gt;
  
  
  计算题
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [joyjoy] agent step 1/10
INFO [joyjoy] tool call: calculator({'expression': '123 * 456 + 789'})
INFO [joyjoy] tool result: 56877
INFO [joyjoy] agent step 2/10

计算结果是 56877。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;--trace&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1] user: 123 * 456 + 789 是多少？
[2] assistant: tool_calls → calculator({"expression":"123 * 456 + 789"})
[3] tool: 56877
[4] assistant: 计算结果是 123 乘以 456 再加 789 等于 56877。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  没有时间信息时
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [joyjoy] agent step 1/10

请提供当前日期或使用可以获取当前日期的服务或工具。
我目前无法直接获取当前日期。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  调优后误调 Calculator
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO [joyjoy] tool call:
calculator({'expression': 'today().day'})

INFO [joyjoy] tool result:
Error: unsupported expression: Attribute

INFO [joyjoy] agent step 2/10

今天是2026-10-22，……
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;代码：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/zealot2002/myagent" rel="noopener noreferrer"&gt;https://github.com/zealot2002/myagent&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ollama</category>
      <category>toolcalling</category>
      <category>python</category>
    </item>
    <item>
      <title>小理发店记账工具，极简操作，欢迎试用</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Tue, 20 Oct 2026 08:32:00 +0000</pubDate>
      <link>https://dev.to/zealot2002/ben-ren-zuo-de-xiao-li-fa-dian-ji-zhang-gong-ju-ji-jian-cao-zuo-huan-ying-shi-yong-1ied</link>
      <guid>https://dev.to/zealot2002/ben-ren-zuo-de-xiao-li-fa-dian-ji-zhang-gong-ju-ji-jian-cao-zuo-huan-ying-shi-yong-1ied</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11z21dyxywfl3nszfuax.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11z21dyxywfl3nszfuax.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7r04htxtg1c9jwlx2rx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk7r04htxtg1c9jwlx2rx.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphyxgd27dnm4p0f1cito.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphyxgd27dnm4p0f1cito.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijsmgjdf6a1dgli72t7h.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fijsmgjdf6a1dgli72t7h.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuk4k3t0xii8xve0dytjb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuk4k3t0xii8xve0dytjb.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtk5fatonskg7jsqvytw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgtk5fatonskg7jsqvytw.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qbwdtah7ye654ygquy7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qbwdtah7ye654ygquy7.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fee2zglpmr7ymf7yvsgq0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fee2zglpmr7ymf7yvsgq0.jpg" alt=" " width="800" height="1069"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;上个月，王姐来剪头发。&lt;br&gt;
剪完我说：「姐，卡里余额不太够了，要不要再充点？」&lt;br&gt;
她愣了一下：「我上礼拜才充的500。」&lt;/p&gt;

&lt;p&gt;我翻开记账本——那笔500，被我记到了李姐那一页。&lt;/p&gt;

&lt;p&gt;当场退钱、道歉，王姐摆摆手笑着说「没事没事」。&lt;br&gt;
可那声「没事」，让我难受了一整晚。&lt;/p&gt;

&lt;p&gt;开小店的人都懂：&lt;br&gt;
老顾客愿意把钱放你这儿，是因为信你。&lt;br&gt;
账错一次，信任就裂一道缝。&lt;/p&gt;

&lt;p&gt;更难的是每天晚上。&lt;br&gt;
收了摊，老婆问今天收了多少，&lt;br&gt;
本子、微信、支付宝翻个遍，计算器按到十一点，还是对不上。&lt;/p&gt;

&lt;p&gt;后来朋友甩给我一个 App：老驴美发会员。&lt;br&gt;
名字挺土，用起来是真顺——&lt;/p&gt;

&lt;p&gt;现在王姐进门：&lt;br&gt;
搜「王姐」，余额直接跳出来，剪发38，点一下就扣完。&lt;br&gt;
她要充值，到账和实收分开记，有赠送一眼能看。&lt;br&gt;
前后不到30秒，后面排队的客人不用等我翻本子。&lt;/p&gt;

&lt;p&gt;散客也有地方去。&lt;br&gt;
点「散客」，现金一笔录进去，收了钱再也不会转头就忘。&lt;/p&gt;

&lt;p&gt;记错了也别慌。&lt;br&gt;
长按那一笔，撤销，余额原路退回。&lt;br&gt;
再也不是拿笔涂改、越改越乱。&lt;/p&gt;

&lt;p&gt;现在晚上收摊，点开「今日经营」：&lt;br&gt;
今天实收多少、充值多少、散客多少，一眼看清。&lt;br&gt;
月底不用按计算器了。&lt;br&gt;
年底还有项目排行——原来烫发才是店里的顶梁柱，明年定价心里有数。&lt;/p&gt;

&lt;p&gt;Android 下载页：&lt;a href="https://hairsalon-ayn.pages.dev/" rel="noopener noreferrer"&gt;https://hairsalon-ayn.pages.dev/&lt;/a&gt;&lt;br&gt;
装上就能用，不用注册登录。&lt;br&gt;
数据就存在自己手机里，不上传云端，顾客名单只有你自己看得到。&lt;br&gt;
店里断网也能记，备份能发到微信。&lt;/p&gt;

&lt;p&gt;唯一要记住的：&lt;br&gt;
数据都在这台手机里，别随手清缓存。&lt;/p&gt;

&lt;p&gt;你们的店，还在拿本子记账吗？&lt;br&gt;
评论区聊聊，你记错过的最离谱的一笔账😌&lt;/p&gt;

</description>
      <category>tools</category>
    </item>
    <item>
      <title>给孩子做的生日3D场景：一个爸爸的技术小实验</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Sat, 04 Jul 2026 01:19:53 +0000</pubDate>
      <link>https://dev.to/zealot2002/gei-hai-zi-zuo-de-sheng-ri-3dchang-jing-ge-ba-ba-de-ji-zhu-xiao-shi-yan-56j3</link>
      <guid>https://dev.to/zealot2002/gei-hai-zi-zuo-de-sheng-ri-3dchang-jing-ge-ba-ba-de-ji-zhu-xiao-shi-yan-56j3</guid>
      <description>&lt;h1&gt;
  
  
  给孩子做的生日3D场景：一个爸爸的技术小实验
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukai5h0gcew18wilqv9d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fukai5h0gcew18wilqv9d.png" alt="完整场景" width="636" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  为什么做这个？
&lt;/h2&gt;

&lt;p&gt;孩子要过生日了，我想给他/她一个特别的礼物。不是买玩具，也不是买蛋糕，而是用技术做一个专属的3D生日场景。&lt;/p&gt;

&lt;p&gt;里面有他/她喜欢的小房子、跑车、直升机，还有一只可爱的小猫。当然，生日蛋糕是必不可少的。&lt;/p&gt;

&lt;h2&gt;
  
  
  这个场景有什么？
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🏠 一个带花园的小房子&lt;/li&gt;
&lt;li&gt;� 生日蛋糕&lt;/li&gt;
&lt;li&gt;� 两辆跑车（一辆白色，一辆黑色）&lt;/li&gt;
&lt;li&gt;� 一只小猫&lt;/li&gt;
&lt;li&gt;� 一架直升机&lt;/li&gt;
&lt;li&gt;👦 一个卡通人物&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1o8bbwzl8vx531foom5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1o8bbwzl8vx531foom5.png" alt="场景细节" width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;孩子可以用鼠标：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;拖拽旋转场景&lt;/li&gt;
&lt;li&gt;滚轮放大缩小&lt;/li&gt;
&lt;li&gt;右键平移&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;就像玩3D游戏一样，可以360度查看每个细节。&lt;/p&gt;

&lt;h2&gt;
  
  
  技术上怎么做的？
&lt;/h2&gt;

&lt;p&gt;简单说就是用了一个叫 Three.js 的JavaScript库，它可以在浏览器里渲染3D场景。&lt;/p&gt;

&lt;p&gt;主要做了几件事：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;加载各种3D模型文件（.glb和.fbx格式）&lt;/li&gt;
&lt;li&gt;把它们放在合适的位置&lt;/li&gt;
&lt;li&gt;加上灯光和阴影&lt;/li&gt;
&lt;li&gt;让模型自动播放动画&lt;/li&gt;
&lt;li&gt;加载时显示进度条，因为模型文件比较大&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjs4fdi2tgzsxg3lubt8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzjs4fdi2tgzsxg3lubt8.png" alt="多模型展示" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  遇到的坑
&lt;/h2&gt;

&lt;p&gt;最大的坑是模型文件太大，加载很慢。所以我加了一个进度条，让孩子知道加载到哪了，不会以为网页坏了。&lt;/p&gt;

&lt;p&gt;还有就是不同模型的尺寸差异很大，需要一个个调整大小和位置，让它们看起来协调。&lt;/p&gt;

&lt;h2&gt;
  
  
  怎么部署？
&lt;/h2&gt;

&lt;p&gt;我用的是 Cloudflare Pages，免费而且速度快。每次push代码到GitHub，它就自动部署了。&lt;/p&gt;

&lt;p&gt;本地运行的话，简单到不行：&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;然后打开浏览器访问 localhost:8080 就行了。&lt;/p&gt;

&lt;h2&gt;
  
  
  孩子的反应
&lt;/h2&gt;

&lt;p&gt;看到这个场景时，他/她眼睛都亮了。指着直升机说"哇"，又指着跑车说"这个好酷"。虽然他/她不懂技术，但这个3D场景确实让他/她开心了。&lt;/p&gt;

&lt;p&gt;这就够了。&lt;/p&gt;

&lt;h2&gt;
  
  
  代码在哪里？
&lt;/h2&gt;

&lt;p&gt;如果你想看看代码，或者自己也做一个，GitHub上有完整的项目：&lt;br&gt;
&lt;a href="https://github.com/zealot2002/3d" rel="noopener noreferrer"&gt;https://github.com/zealot2002/3d&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;技术不重要，重要的是用心给孩子做点特别的东西。&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>threejs</category>
      <category>webgl</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a pure-frontend, all-in-one toolbox for the tasks I reach for every day</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Wed, 10 Jun 2026 09:02:26 +0000</pubDate>
      <link>https://dev.to/zealot2002/appforge-app-dev-workstation-for-mobile-developers-82h</link>
      <guid>https://dev.to/zealot2002/appforge-app-dev-workstation-for-mobile-developers-82h</guid>
      <description>&lt;p&gt;After years of mobile development, one of the most annoying parts of my daily workflow is jumping between scattered tool websites.&lt;/p&gt;

&lt;p&gt;Finding image assets means digging through folders layer by layer — SVG and Lottie need separate preview tools. Converting or compressing images means uploading to online converters. Debugging APIs means opening a JSON formatter. Inspecting an APK means yet another dedicated site. By the end of the day I have a dozen tabs open, constantly context-switching. Worse, many online tools require file uploads — internal project assets and unreleased builds always feel risky.&lt;/p&gt;

&lt;p&gt;When things slowed down recently, I built a pure-frontend, all-in-one toolbox for the tasks I reach for every day. I call it &lt;strong&gt;AppForge&lt;/strong&gt;. Core features run locally in the browser — files never leave your machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kba2td6dynp34mcvprc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kba2td6dynp34mcvprc.png" alt="AppForge homepage — AI picks + dev tools" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resource Preview: find one icon in 2,870 files
&lt;/h2&gt;

&lt;p&gt;This was the first module I built — and the one I use most — because it hits a daily pain point for our &lt;strong&gt;40-person app team&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One project scan surfaces &lt;strong&gt;2,870&lt;/strong&gt; resource files. Search &lt;code&gt;close&lt;/code&gt; and you get &lt;strong&gt;80+&lt;/strong&gt; near-identical close icons: inconsistent names, scattered paths, zero discipline. Can I push the whole team to clean this up overnight? Honestly, no.&lt;/p&gt;

&lt;p&gt;But one missing asset blocks your whole flow. My take is simple: &lt;strong&gt;if I can't fix the mess, I can at least find what I need in seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select a local project folder — AppForge scans everything and recognizes SVG, WebP, PNG, GIF, Lottie, Vector, Iconfont, and more. The screenshot shows it in action: search &lt;code&gt;close&lt;/code&gt;, filter Vector, 81 matches with instant preview and full paths. No Finder spelunking, no guessing the rest of the filename.&lt;/p&gt;

&lt;p&gt;Results cache locally — reopen and you're ready. Onboarding legacy projects, auditing duplicate assets, or chasing "that little X close icon" — what used to take minutes now takes seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1eh6vnn4rnizeztjfqq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp1eh6vnn4rnizeztjfqq.png" alt="Resource preview — search close among 2,870 assets" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  File Conversion: image processing in one place
&lt;/h2&gt;

&lt;p&gt;After finding assets comes format work: SVG → PNG for multi-platform, PNG → WebP for smaller bundles, HEIC → common formats, occasional background removal for marketing assets.&lt;/p&gt;

&lt;p&gt;Instead of design software or multiple upload sites, the convert module handles format conversion, smart compression, and background removal — all locally. Drop files in, download results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fti4y41pir6fawkp9m5t8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fti4y41pir6fawkp9m5t8.png" alt="File conversion — formats, HEIC, compression" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  APK Inspector: unpack locally
&lt;/h2&gt;

&lt;p&gt;Android devs regularly need to inspect APKs: competitor package names and versions, signatures and permissions, icons and resources.&lt;/p&gt;

&lt;p&gt;Drag an APK into AppForge to view package info, Manifest, permissions, components, and export internal assets. Need decompilation? One click to JADX.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fds71pnaqfag6p7yypux6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fds71pnaqfag6p7yypux6.png" alt="APK inspector — package info, permissions, components" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Image OCR: copy text from screenshots
&lt;/h2&gt;

&lt;p&gt;Stack traces, design copy, code snippets in docs — often trapped in screenshots. The OCR module runs locally, supports Chinese and English, batch processing, one-click copy. Files stay on your machine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxq4s000s179hrmch9jr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxq4s000s179hrmch9jr8.png" alt="Image OCR — Chinese &amp;amp; English" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Mini Tools: no more bookmark clutter
&lt;/h2&gt;

&lt;p&gt;JSON formatting, Base64 / URL / Unicode encode-decode, regex testing, timestamp conversion — small but essential. Built-in presets for email, phone, ID, IP, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsfakh9zla4stoi1y9k9z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsfakh9zla4stoi1y9k9z.png" alt="Mini tools — JSON, regex, Base64, and more" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Curated Tech Articles: skip the noise
&lt;/h2&gt;

&lt;p&gt;Another big pain point. As a mobile developer I skim tons of tech content daily — most of it is fluff, reposts, or clickbait.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;AI-curated articles&lt;/strong&gt; section pulls candidates from DEV and Hacker News, scores each post with an LLM, and keeps only high-quality picks. &lt;strong&gt;Updated daily with cumulative history&lt;/strong&gt; — past picks aren't wiped each run. Browse by category in Chinese or English — like having someone pre-filter for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauu49rgxkvk965lrbr0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fauu49rgxkvk965lrbr0z.png" alt="AI-curated articles — categories, scores, and reasons" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Open APIs: quick reference for demos
&lt;/h2&gt;

&lt;p&gt;Building a small demo often means hunting for free APIs, testing endpoints, bookmarking docs. I curated common APIs by category with sample calls — look up when you need them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwv3vpuo7gwszu1of2ik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwv3vpuo7gwszu1of2ik.png" alt="Open APIs — categorized quick reference" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I built it this way
&lt;/h2&gt;

&lt;p&gt;Simple goal: &lt;strong&gt;make my own workflow smoother&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local-first&lt;/strong&gt;: sensitive project assets and builds stay in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero friction&lt;/strong&gt;: no signup, no desktop install — just open and use&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real scenarios&lt;/strong&gt;: every feature comes from actual daily pain points&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with &lt;strong&gt;React 19 + Vite 8 + Tailwind CSS 4&lt;/strong&gt;, deployed on Cloudflare Pages, with lazy loading, caching, and bilingual UI.&lt;/p&gt;

&lt;p&gt;Started as a personal toolbox — after using it for a while I polished it for others. If you're a mobile developer tired of scattered tools, give it a try.&lt;/p&gt;

&lt;p&gt;What's missing from your dev workstation? Share the tools and workflow tricks you can't live without in the comments — let's swap notes and level up together.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;👉 Try it now: &lt;a href="https://appforge-7tw.pages.dev" rel="noopener noreferrer"&gt;https://appforge-7tw.pages.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AppForge&lt;/strong&gt; — AI-curated reads and dev tools at your fingertips, all-in-one.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Modern Android Architecture (Part 4): Design Patterns — The Glue of Lego Architecture</title>
      <dc:creator>goodlords</dc:creator>
      <pubDate>Mon, 01 Jun 2026 02:06:48 +0000</pubDate>
      <link>https://dev.to/zealot2002/modern-android-architecture-part-4-design-patterns-the-glue-of-lego-architecture-1e9f</link>
      <guid>https://dev.to/zealot2002/modern-android-architecture-part-4-design-patterns-the-glue-of-lego-architecture-1e9f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Series Navigation&lt;/strong&gt;: &lt;a href="https://dev.to/zealot2002/modern-android-architecture-part-1-a-decade-of-android-architecture-evolution-what-problem-are-403f"&gt;Article 1: A Decade of Android Architecture Evolution&lt;/a&gt; | &lt;a href="https://dev.to/zealot2002/modern-android-architecture-part-2-the-lego-architecture-divide-and-conquer-taken-to-the-58j7"&gt;Article 2: The Lego Architecture: Divide and Conquer, Taken to the Extreme&lt;/a&gt; | &lt;a href="https://dev.to/zealot2002/modern-android-architecture-part-3-refactoring-a-product-detail-page-with-lego-architecture-4ii4"&gt;Article 3: Refactoring a Product Detail Page with Lego Architecture&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Repository&lt;/strong&gt;: &lt;a href="https://github.com/zealot2002/androidArch" rel="noopener noreferrer"&gt;https://github.com/zealot2002/androidArch&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Foreword: Bricks Are Ready, How to Assemble Them Stably?
&lt;/h2&gt;

&lt;p&gt;In the first three articles, we followed a clear path:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Article 1&lt;/strong&gt; pointed out: Architecture is just "technique" — it can manage layering, but not splitting granularity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article 2&lt;/strong&gt; proposed Lego Architecture: Split infinitely to minimum particles, making reuse visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article 3&lt;/strong&gt; proved with practical product detail page: Bricks like Mapper, Assembler, ListItem can indeed split a 3000-line disaster into 15 independent components.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there's a question hinted at the end of Article 3 — &lt;strong&gt;Splitting is only the first step; assembly determines whether this system can run long-term.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two pages both require "login before operation" — do you plan to copy-paste two sets of &lt;code&gt;if (isLogin)&lt;/code&gt; checks? Three types of posters (product, social, store) share the same "render → screenshot → preview → share" workflow, but with completely different UI layouts — do you plan to write three &lt;code&gt;when&lt;/code&gt; branches in Activity, each hundreds of lines long?&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;design patterns&lt;/strong&gt; come in. They are not another "silver bullet architecture," but the &lt;strong&gt;glue&lt;/strong&gt; between Lego bricks — using minimal, proven connection methods to stably assemble already-split particles together.&lt;/p&gt;

&lt;p&gt;The 23 GoF patterns and more evolving patterns each solve different "assembly" problems: Factory is responsible for &lt;strong&gt;creating&lt;/strong&gt; the right bricks, Strategy for &lt;strong&gt;switching&lt;/strong&gt; replaceable behaviors, Adapter for &lt;strong&gt;connecting&lt;/strong&gt; incompatible interfaces, Observer for &lt;strong&gt;responding&lt;/strong&gt; to state changes, Template Method for &lt;strong&gt;solidifying&lt;/strong&gt; invariant processes... Due to space constraints, this article won't cover all of them, but will select two representative cases from the demo project — &lt;strong&gt;Observer&lt;/strong&gt; and &lt;strong&gt;Template Method&lt;/strong&gt; — to illustrate how design patterns generally work in Lego Architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rp863k7hyghawg1jnx2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6rp863k7hyghawg1jnx2.png" alt="Demo project screenshot" width="799" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: Product detail page from Article 3 — favorite, add to cart, and buy now gated by &lt;code&gt;LoginRouter&lt;/code&gt;; Right: Three poster share pages (product / social / store) — sharing the &lt;code&gt;BillActivity&lt;/code&gt; screenshot flow, each rendered by a different &lt;code&gt;BaseBillRender&lt;/code&gt; subclass.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  I. Observer Pattern: LoginRouter — The "Broadcast Station" for Login State
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Pain Point: Login Interception Scattered Everywhere
&lt;/h3&gt;

&lt;p&gt;In e-commerce apps, many operations depend on login state: favorite products, add to cart, buy now, post comments...&lt;/p&gt;

&lt;p&gt;The crudest approach is to repeat this check in every click event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isLogin&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LoginActivity&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problems are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repetition&lt;/strong&gt;: Ten buttons, ten copies of the same check logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken flow&lt;/strong&gt;: After successful login, the original operation is often "lost" — the user has to click the button again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coupling&lt;/strong&gt;: Some projects even write post-login navigation directly into &lt;code&gt;LoginActivity&lt;/code&gt;, coupling the login module with other modules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lego Architecture requires: &lt;strong&gt;Extract "execute after login" as an independent, reusable brick.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Solution: Observer Pattern
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;LoginRouter&lt;/code&gt; consists of two observable sources (&lt;code&gt;LifecycleOwner&lt;/code&gt;, &lt;code&gt;LoginStateLiveData&lt;/code&gt;) and a &lt;code&gt;pendingBlock&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LoginStateLiveData&lt;/code&gt;&lt;/strong&gt; — Subscribe to login state. Publish &lt;code&gt;true&lt;/code&gt; after successful login to trigger pending callbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;LifecycleOwner&lt;/code&gt;&lt;/strong&gt; — Subscribe to page lifecycle. Clear pending when page is destroyed to avoid leaks; also clear on &lt;code&gt;ON_RESUME&lt;/code&gt; — when user presses back from login page without logging in, pending operations should be abandoned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pendingBlock&lt;/code&gt;&lt;/strong&gt; — Store the "execute after login" operation. Suspended when not logged in, automatically &lt;code&gt;invoke()&lt;/code&gt; after successful login.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Core code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoginRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;pendingBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;LifecycleOwner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Observer: Login success → execute pending operation&lt;/span&gt;
            &lt;span class="nc"&gt;LoginStateLiveData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;LifecycleOwner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;pendingBlock&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="n"&gt;pendingBlock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c1"&gt;// ON_RESUME: User returns from login page but not logged in (e.g., pressed back), abandon pending&lt;/span&gt;
            &lt;span class="c1"&gt;// ON_DESTROY: Page destroyed, clean up pending&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;LifecycleOwner&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;lifecycle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;LifecycleEventObserver&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;Lifecycle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ON_RESUME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nc"&gt;Lifecycle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ON_DESTROY&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pendingBlock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
                        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;runBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;LifecycleOwner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LoginStateLiveData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;pendingBlock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;
                &lt;span class="nc"&gt;AppRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After successful login, the login page only needs to update the global state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LoginActivity&lt;/span&gt;
&lt;span class="nc"&gt;LoginStateLiveData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is the complete Observer Pattern loop:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LoginActivity login successful
       │
       ▼
LoginStateLiveData.value = true   ← Subject publishes state
       │
       ▼
LoginRouter receives notification ← Observer
       │
       ▼
pendingBlock?.invoke()            ← Automatically execute suspended operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1.3 Usage in Product Detail Page
&lt;/h3&gt;

&lt;p&gt;In the refactored &lt;code&gt;GoodsDetailActivity&lt;/code&gt; from Article 3, favorite, add to cart, and buy now are all handled with one line through &lt;code&gt;LoginRouter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;loginRouter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LoginRouter&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;LoginRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Favorite&lt;/span&gt;
&lt;span class="n"&gt;loginRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runBlock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Note: this is not a page navigation — it triggers a state update after login&lt;/span&gt;
    &lt;span class="n"&gt;favViewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFavorite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentSpuId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetFavorite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Add to cart&lt;/span&gt;
&lt;span class="n"&gt;loginRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runBlock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ToastUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;goods_action_add_cart_hint&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Buy now → Navigate to confirm order page after login&lt;/span&gt;
&lt;span class="n"&gt;loginRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runBlock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AppRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openConfirmOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference between &lt;code&gt;LoginRouter&lt;/code&gt; and a route interceptor: interceptors usually only handle unified page navigation, while &lt;code&gt;runBlock&lt;/code&gt; can queue any operation as pending — favorite, API request, show Toast, not limited to opening new pages. Subsequent actions are still declared locally in the page, and &lt;code&gt;LoginActivity&lt;/code&gt; no longer needs to act as a transit hub. Module boundaries are clear and decoupled.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.4 Origin and Boundaries
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;LoginRouter&lt;/code&gt; wasn't designed upfront; it emerged from project pain points.&lt;/p&gt;

&lt;p&gt;After connecting favorite, add to cart, and buy now to the detail page, the same logic repeatedly appeared across multiple buttons: check login, navigate to login page, continue original operation after success. Interceptors could only intercept navigation uniformly, not handle "continue favorite after login"; writing destinations into &lt;code&gt;LoginActivity&lt;/code&gt; would couple the login module with business logic. After several rounds of copy-paste, the pain point became clear — &lt;strong&gt;missing a common brick that can queue any follow-up action as pending.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Extract, observe, abstract: &lt;code&gt;LoginRouter&lt;/code&gt; combines login guard and pending continuation into one brick, using &lt;code&gt;LoginStateLiveData&lt;/code&gt; and &lt;code&gt;LifecycleOwner&lt;/code&gt; to coordinate timing, elegantly solving this pain point.&lt;/p&gt;

&lt;p&gt;Lego Architecture not only requires the ability to split, but also to see patterns in repetition and discover bricks in pain points — &lt;code&gt;LoginRouter&lt;/code&gt; is one such refinement.&lt;/p&gt;




&lt;h2&gt;
  
  
  II. Template Method Pattern: BillActivity — Fixed Process, Variable Content
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Pain Point: Three Poster Types, One Workflow
&lt;/h3&gt;

&lt;p&gt;We added a &lt;strong&gt;poster sharing&lt;/strong&gt; feature to the demo project — users can generate beautiful posters for products, social posts, or store pages, save to album, or share to WeChat.&lt;/p&gt;

&lt;p&gt;The three poster types have completely different UI layouts, but &lt;strong&gt;the page-level workflow is identical&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Load data → Select Render → Bind view → Wait for render ready → Screenshot → Preview → Save/Share
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we write three sets of rendering logic in &lt;code&gt;BillActivity&lt;/code&gt; using &lt;code&gt;when (billCase)&lt;/code&gt;, the Activity will quickly bloat; if we write a separate Activity for each poster type, the screenshot, preview, and save code will be duplicated three times.&lt;/p&gt;

&lt;p&gt;Template Method Pattern solution: &lt;strong&gt;Solidify the "invariant process" in a template, leave "variable parts" for subclasses to implement.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Architecture Layers: Interface → Abstract Template → Concrete Implementation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Step 1: Define Brick Interface
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BillRender&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onBindView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getBillView&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt;

    &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Listener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;screenReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bgBitmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bitmap&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BillRender&lt;/code&gt; is a standard interface — all poster Render bricks must follow these two methods.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Abstract Template Class Solidifies Common Skeleton
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseBillRender&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;B&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewBinding&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BillRender&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;lateinit&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;B&lt;/span&gt;

    &lt;span class="c1"&gt;// Subclasses only implement this method — fill specific UI&lt;/span&gt;
    &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onRenderView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BillRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Template method: Reflection automatically inflates corresponding ViewBinding&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;superclass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;javaClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;genericSuperclass&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;ParameterizedType&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bindingClass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;superclass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actualTypeArguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;B&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;inflate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bindingClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDeclaredMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"inflate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LayoutInflater&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inflate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;layoutInflater&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;B&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onBindView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BillRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;onRenderView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Call subclass implementation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getBillView&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;View&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This is the core of Template Method Pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BaseBillRender&lt;/code&gt; defines the &lt;strong&gt;algorithm skeleton&lt;/strong&gt; (inflate Binding → call &lt;code&gt;onRenderView&lt;/code&gt; → expose &lt;code&gt;getBillView&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Subclasses only need to implement &lt;strong&gt;one hook:&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;onRenderView&lt;/code&gt;&lt;/strong&gt;, filling in their own UI logic.&lt;/li&gt;
&lt;li&gt;Common ViewBinding initialization is done through generics + reflection, with zero boilerplate code for subclasses.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Step 3: Concrete Subclasses Only Care About Their UI, Need Only Implement One Method
&lt;/h4&gt;

&lt;p&gt;Taking product poster as example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GoodsBillRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;BaseBillRender&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GoodsBillData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;LayoutBillGoodsBinding&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onRenderView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GoodsBillData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LayoutBillGoodsBinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BillRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvTitle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tvPrice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
        &lt;span class="c1"&gt;// ... Fill product-specific fields&lt;/span&gt;

        &lt;span class="c1"&gt;// Wait for layout drawing + network image loading to complete&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ivCover&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadNetworkImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onSuccess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;markReady&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ivQrCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadNetworkImageCircle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;miniProgramCodeUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onSuccess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;markReady&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="c1"&gt;// Call screenReady when all ready&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;markReady()&lt;/code&gt; internally maintains a counter (product poster needs to wait for layout drawing + cover image + QR code = 3 items), and only triggers &lt;code&gt;screenReady&lt;/code&gt; when the count is complete, ensuring complete content during screenshot.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SocialBillRender&lt;/code&gt; and &lt;code&gt;ShopBillRender&lt;/code&gt; have the same structure, each binding different layouts and Data types. &lt;strong&gt;Adding a new poster type only requires adding a new Render subclass + one Layout XML, zero modifications to existing code.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 4: Factory Selects Concrete Brick
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;BillRenderFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;BillRender&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;RouterConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BILL_CASE_SOCIAL&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;SocialBillRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;RouterConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BILL_CASE_SHOP&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ShopBillRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;GoodsBillRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.3 BillActivity: Orchestrating the Fixed Workflow
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;BillActivity&lt;/code&gt; itself contains no specific poster UI logic; it only &lt;strong&gt;orchestrates the template process&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BillActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... Initialize views, share button&lt;/span&gt;

        &lt;span class="c1"&gt;// 1. Factory selects Render brick&lt;/span&gt;
        &lt;span class="n"&gt;billRender&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BillRenderFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;billCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flBillContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBillView&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Load data + bind view&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;billData&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BillDataLoader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;billId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;billRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onBindView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="err"&gt;: &lt;/span&gt;&lt;span class="nc"&gt;BillRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Listener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;screenReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bgBitmap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bitmap&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;captureBillSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// 3. Render ready → screenshot&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;captureBillSnapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 4. View → Bitmap → Preview display&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bitmap&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BillBitmapUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;viewToBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;billRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBillView&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ivBill&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// 5. Show bottom save/share bar&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The responsibility boundaries are clear throughout the page:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BillDataLoader&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Load Mock data by case&lt;/td&gt;
&lt;td&gt;Simple Factory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BillRenderFactory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Select specific Render implementation&lt;/td&gt;
&lt;td&gt;Simple Factory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BaseBillRender&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Solidify inflate + bind skeleton&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Template Method&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GoodsBillRender&lt;/code&gt;, etc.&lt;/td&gt;
&lt;td&gt;Fill specific UI + async ready detection&lt;/td&gt;
&lt;td&gt;Template Method hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BillActivity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Orchestrate load → render → screenshot → preview → share&lt;/td&gt;
&lt;td&gt;Process controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;BillBitmapUtils&lt;/code&gt; / &lt;code&gt;BillImageSaver&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Screenshot and save&lt;/td&gt;
&lt;td&gt;Independent utility bricks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2.4 Echo with Article 2: When to Use Template Method?
&lt;/h3&gt;

&lt;p&gt;In Article 2, we harshly criticized this kind of Base:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppCompatActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;initCallerData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// Forced order&lt;/span&gt;
        &lt;span class="nf"&gt;initView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;initPageData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;initObservers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;initView&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;initPageData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why is the template method in &lt;code&gt;BaseBillRender&lt;/code&gt; acceptable, but not in &lt;code&gt;BaseActivity&lt;/code&gt;?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;br&gt;&lt;/th&gt;
&lt;th&gt;Bad BaseActivity Template&lt;/th&gt;
&lt;th&gt;Good BaseBillRender Template&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Is the process really consistent?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Different pages have very different initialization orders&lt;/td&gt;
&lt;td&gt;All posters must inflate → bind → render → ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Constrains order or structure?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Forces &lt;code&gt;initView&lt;/code&gt; before &lt;code&gt;initData&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Only specifies "call &lt;code&gt;onRenderView&lt;/code&gt; when binding data"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subclass freedom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kidnapped by four abstract methods&lt;/td&gt;
&lt;td&gt;Only need to implement one &lt;code&gt;onRenderView&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Is it pluggable?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must inherit Base to use&lt;/td&gt;
&lt;td&gt;Just implement &lt;code&gt;BillRender&lt;/code&gt; interface, no inheritance required&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The judgment is simple: Only use template method when the algorithm skeleton is truly consistent across multiple subclasses; if you're just "incidentally" stuffing a bunch of optional initialization steps into Base, it's a shackle, not a template.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  III. Gluing Approaches from Two Cases
&lt;/h2&gt;

&lt;p&gt;The two cases above demonstrate two common "gluing" directions of design patterns in Lego Architecture — &lt;strong&gt;responding to changes&lt;/strong&gt; and &lt;strong&gt;solidifying processes&lt;/strong&gt;. Other patterns solve different dimensions of assembly problems like creation, replacement, adaptation, etc., with similar thinking.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Observer (LoginRouter)&lt;/th&gt;
&lt;th&gt;Template Method (BillRender)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Problem Solved&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When A's state changes, B needs to respond automatically&lt;/td&gt;
&lt;td&gt;Multiple subclasses share the same algorithm skeleton, only some steps differ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subscription / Callback&lt;/td&gt;
&lt;td&gt;Inherit abstract class + implement hook methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coupling Direction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subject doesn't know who's observing (unidirectional)&lt;/td&gt;
&lt;td&gt;Subclass depends on parent skeleton (unidirectional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lego Position&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cross-module event broadcasting brick&lt;/td&gt;
&lt;td&gt;Common skeleton brick for similar Renders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extension Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Add new observers without modifying Subject&lt;/td&gt;
&lt;td&gt;Add new subclasses without modifying template skeleton&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Regardless of pattern, there's a common prerequisite: &lt;strong&gt;They are used to "glue" bricks after Lego splitting — not to replace splitting itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you haven't split login logic into &lt;code&gt;LoginRouter&lt;/code&gt;, Observer Pattern won't save you — you're just writing LiveData observations in three Activities. If you haven't split the three posters into independent Renders, Template Method won't save you either — &lt;code&gt;BaseBillRender&lt;/code&gt; will be filled with &lt;code&gt;if (case == SOCIAL)&lt;/code&gt; branches. Same for Factory, Strategy, and other patterns — &lt;strong&gt;Split first, then glue, order matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Beyond the two cases detailed in this article, the demo project has patterns working silently throughout — they're not the stars, but they're connectors between bricks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Location in Project&lt;/th&gt;
&lt;th&gt;What It Glues&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LoginRouter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Login state changes → cross-page pending follow-up operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Template Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BaseBillRender&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multiple posters share inflate → bind → render skeleton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Simple Factory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BillRenderFactory&lt;/code&gt;, &lt;code&gt;BillDataLoader&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Create corresponding Render / data by &lt;code&gt;billCase&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strategy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;BillRender&lt;/code&gt; and its implementations&lt;/td&gt;
&lt;td&gt;Switch different poster rendering strategies under the same screenshot process&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Adapter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GoodsDetailAdapter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14 types of &lt;code&gt;ListItem&lt;/code&gt; → RecyclerView multi-type rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  IV. Complete Lego Architecture System: Four Articles Connected
&lt;/h2&gt;

&lt;p&gt;At this point, the four articles form a complete narrative:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Article 1: Problem Awareness
  └─ Architecture is just "technique", connection count determines complexity

Article 2: Splitting Methodology
  └─ Lego Architecture = Infinite splitting to minimum particles + Governance iteration

Article 3: Splitting Practice
  └─ Product Detail Page: ListItem + Mapper + Assembler + ViewModel

Article 4: The Glue
  └─ Design Patterns: On top of splitting, use appropriate patterns to stably assemble bricks (this article focuses on Observer and Template Method)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete Lego Architecture system can be summarized as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One Axiom&lt;/strong&gt;: Divide-and-conquer + Single Responsibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Theorems&lt;/strong&gt;: Governance thinking, tool iteration (Private→Shared→Remote), reuse discovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One Practice&lt;/strong&gt;: 15 independent components in product detail page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One Layer of Glue&lt;/strong&gt;: Design patterns — this article focuses on Observer and Template Method; the project also uses Factory, Strategy, Adapter patterns for connection (see table above)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you truly build applications with Lego thinking, you'll find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complexity doesn't come from the application itself&lt;/strong&gt;, but from not splitting it into small enough bricks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design patterns aren't for showing off&lt;/strong&gt;, but for selecting appropriate patterns by scenario after splitting, using minimal connections to stably assemble bricks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good code isn't written by learning one architecture&lt;/strong&gt;, but by having divide-and-conquer awareness, governance discipline, pattern intuition, and the ability to spot reusable bricks.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Series Conclusion
&lt;/h2&gt;

&lt;p&gt;Four articles, from "Architecture Evolution History" to "Lego Splitting Methodology", to "Product Detail Page Practice", and finally to "Design Pattern Glue" — we've tried to answer a consistent question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to truly improve Android code quality?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer isn't in the labels of MVVM or MVI, but in daily programming discipline: split until you can't split anymore, glue only when needed, govern for sustainable iteration.&lt;/p&gt;

&lt;p&gt;May your codebase be like a box of Lego — each brick has clear responsibilities, stable interfaces, can be combined freely, and can still build new models after many years.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related Reading&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/zealot2002/modern-android-architecture-part-1-a-decade-of-android-architecture-evolution-what-problem-are-403f"&gt;Article 1: A Decade of Android Architecture Evolution: What Problem Are We Really Solving?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/zealot2002/modern-android-architecture-part-2-the-lego-architecture-divide-and-conquer-taken-to-the-58j7"&gt;Article 2: The Lego Architecture: Divide and Conquer, Taken to the Extreme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/zealot2002/modern-android-architecture-part-3-refactoring-a-product-detail-page-with-lego-architecture-4ii4"&gt;Article 3: Refactoring a Product Detail Page with Lego Architecture: From 3000 Lines to 15 Standalone Components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
