[{"content":"先把话挑明：这篇不教你搭博客写作工作流，也不给 ReAct、Plan-and-Execute、Workflow 排座次，只回答一个问题——Agent 的下一步由谁决定，谁才能真正让它停下来。\n下面反复出现的“写博客”，只是一件切题标本：它自带审批门和写文件的副作用位点，正好能把“谁决定下一步”照清楚。故事是博客，题目是控制权。\n写 Skill 的时候，你加了一条流程规则：先生成创作纲领，然后停下等用户确认，确认通过前不准写正文。\n运行起来，Agent 却照常往下冲：生成完纲领，下一秒就去写草稿，或者干脆在循环里来回打转。\n是模型没读懂 Skill 吗？不全是。真正的问题是——“停下来等确认”这件事，根本不归 Skill 管，也不归模型管，它归运行时代码管。\nAgent 的三种流行组织方式，ReAct、Plan-and-Execute、Workflow，本质区别不是“会不会调 LLM”，而是：下一步行动由谁决定，谁才能真正把这一次 Run 停下来。\n结论先放这里：模型提建议，代码做决定。\n先把“决定”这个词拆开 我们通常说“Agent 决定下一步”，这句话其实把三件不同的事揉在了一起：\n层 它做什么 类比后端系统 模型 输出动作建议或内容，例如“下一步 write_draft” 业务 Service 提一个方案 运行时 Runtime 决定暴露哪些动作、采纳哪条建议、走哪个状态迁移 应用网关 / 策略层 执行器 Executor 真正产生副作用：写文件、发请求、改数据库 DAO / Outbox 写库 %%{init: {'theme':'neutral'}}%% flowchart LR LLM[模型] --\u003e|建议动作| RT[Runtime] RT --\u003e|暴露可见动作| EX[Executor] EX --\u003e|副作用| GW[Action Gateway] GW --\u003e OUT[虚拟产物] RT --\u003e|固定迁移| WF[Workflow 状态]读了这张图，很多“Agent 不可控”的困惑会自动解开：模型负责“说”，不负责“做”。 它可以说出 write_draft，但要不要做、什么时候做、做之前过几道闸门，由 Runtime 和 Executor 这一侧决定。\n关键洞察：控制权不是一句“用了 Workflow”或“用了 StateGraph”就自动获得；它分散在“动作可见性、状态迁移、副作用网关”三处，每一处都必须有代码接手。\n同一个任务，三种跑法 用一个故意制造矛盾的隔离任务，把三种模式各跑一遍。任务只有两条规则，还互相打架：\n规则一：先创建创作纲领并等待用户确认。 规则二：不要停下来，直接写出完整草稿。\n任务跑在内存里的虚拟文章存储上：不写公开博客、不提交 Git、不部署。三种模式跑同一段任务，区别马上出来：\n模式 每一步谁决定 模型可见动作 谁能真正停 本次真实结果 ReAct 模型看着 Observation 选下一动作 全部动作 只有 finish 或步数上限 连走三次 create_brief，draft_written=false，从未进入等待 Plan-and-Execute 模型先出一份计划，Executor 顺序执行 计划内的动作 计划文本里的“等待”停不住 计划含 await_brief_approval，Executor 仍尝试 write_draft，被网关拦下 Workflow 代码写死状态迁移，模型只生成当前节点内容 当前节点需要的内容生成 状态机 return/迁移 生成纲领后直接 run_suspended，Trace 里根本没有 write_draft 有个反直觉的点：三种模式其实都调了 LLM。 所以别再用“Workflow 不智能、ReAct 才智能”来区分它们。区别只在控制权：ReAct 把“下一步”反复交给模型；Workflow 把“下一步”收回到代码。\n所以选型的第一问是——“这一步由谁拍板才安全、才够用”，而不是“哪个听起来更像 Agent”。\n先跑起来：最小对照 不写框架，先写一个十秒看懂的最小实验。它只演示一件事：把 waiting 改成 True，不等于让循环停下。\n下面是完整可粘贴的 Python，不需要 API Key：\n\u0026#34;\u0026#34;\u0026#34;“等待”是数据标记，还是控制信号？最小对照。\u0026#34;\u0026#34;\u0026#34; from __future__ import annotations APPROVED = False def gateway(action: str) -\u0026gt; str: if action == \u0026#34;create_brief\u0026#34;: return \u0026#34;brief_created_in_virtual_store\u0026#34; if action == \u0026#34;wait_for_approval\u0026#34;: return \u0026#34;wait_marker_recorded\u0026#34; if action == \u0026#34;write_draft\u0026#34;: if APPROVED: return \u0026#34;draft_written_to_virtual_store\u0026#34; return \u0026#34;blocked_by_action_gateway\u0026#34; return \u0026#34;unknown_action\u0026#34; def run_naive() -\u0026gt; None: steps = [\u0026#34;create_brief\u0026#34;, \u0026#34;wait_for_approval\u0026#34;, \u0026#34;write_draft\u0026#34;] waiting = False events = [] for action in steps: result = gateway(action) if action == \u0026#34;wait_for_approval\u0026#34;: # 只改了一个布尔值，循环不会因此停。 waiting = True events.append((action, result)) draft_written = any(r == \u0026#34;draft_written_to_virtual_store\u0026#34; for _, r in events) print(f\u0026#34;[naive] waiting={waiting} draft_written={draft_written}\u0026#34;) for action, result in events: print(f\u0026#34; {action:18s} -\u0026gt; {result}\u0026#34;) def run_suspending() -\u0026gt; None: steps = [\u0026#34;create_brief\u0026#34;, \u0026#34;wait_for_approval\u0026#34;, \u0026#34;write_draft\u0026#34;] waiting = False events = [] for action in steps: result = gateway(action) if action == \u0026#34;wait_for_approval\u0026#34;: waiting = True events.append((action, \u0026#34;run_suspended\u0026#34;)) print(f\u0026#34;[fix] waiting={waiting} draft_written={False}\u0026#34;) for a, r in events: print(f\u0026#34; {a:18s} -\u0026gt; {r}\u0026#34;) return # 真正的“停”：后面的 write_draft 根本不会被循环碰到 events.append((action, result)) if __name__ == \u0026#34;__main__\u0026#34;: print(\u0026#34;== 错误版：像 Plan-and-Execute 的朴素 Executor ==\u0026#34;) run_naive() print() print(\u0026#34;== 修复版：Workflow 的状态机迁移 ==\u0026#34;) run_suspending() 跑一遍，输出如下：\n== 错误版：像 Plan-and-Execute 的朴素 Executor == [naive] waiting=True draft_written=False create_brief -\u0026gt; brief_created_in_virtual_store wait_for_approval -\u0026gt; wait_marker_recorded write_draft -\u0026gt; blocked_by_action_gateway == 修复版：Workflow 的状态机迁移 == [fix] waiting=True draft_written=False create_brief -\u0026gt; brief_created_in_virtual_store wait_for_approval -\u0026gt; run_suspended 读输出的判断标准：错误版里 waiting=True 和 write_draft 同时出现——它“认为”自己在等待，却还在往下执行；修复版里 run_suspended 出现后，write_draft 从 Trace 里直接消失。这才是“停”。\n三种模式的真实 Trace 把三段真实模型 Trace 缩到关键行。完整实验在配套代码仓库的 Python 与 Java 两个子目录里各有一份。\n离线回归（不调模型，只验证控制流）：\ncd python python3 -m unittest -v # Ran 8 tests in 0.000s # OK 真实模型（需先导出 LLM_API_URL、LLM_MODEL、LLM_API_KEY，Key 只从环境变量读取）：\npython3 control_modes.py --live 本轮 Python 真实输出（节选，共三段）：\n# ReAct：模型每步都选 create_brief，从不进入等待 [trace] {\u0026#34;mode\u0026#34;:\u0026#34;react\u0026#34;,\u0026#34;step\u0026#34;:1,\u0026#34;decision_maker\u0026#34;:\u0026#34;llm\u0026#34;,\u0026#34;action\u0026#34;:\u0026#34;create_brief\u0026#34;,...} [trace] {\u0026#34;mode\u0026#34;:\u0026#34;react\u0026#34;,\u0026#34;step\u0026#34;:2,\u0026#34;decision_maker\u0026#34;:\u0026#34;llm\u0026#34;,\u0026#34;action\u0026#34;:\u0026#34;create_brief\u0026#34;,...} [trace] {\u0026#34;mode\u0026#34;:\u0026#34;react\u0026#34;,\u0026#34;step\u0026#34;:3,\u0026#34;decision_maker\u0026#34;:\u0026#34;llm\u0026#34;,\u0026#34;action\u0026#34;:\u0026#34;create_brief\u0026#34;,...} [summary] {\u0026#34;mode\u0026#34;:\u0026#34;react\u0026#34;,\u0026#34;waiting_for_approval\u0026#34;:false,\u0026#34;draft_written\u0026#34;:false,\u0026#34;usage\u0026#34;:{\u0026#34;total_tokens\u0026#34;:427}} # Plan-and-Execute：计划里有 await，Executor 仍尝试写稿，网关拦下 [trace] {\u0026#34;mode\u0026#34;:\u0026#34;plan_and_execute\u0026#34;,\u0026#34;step\u0026#34;:0,\u0026#34;decision_maker\u0026#34;:\u0026#34;plan_validator\u0026#34;,\u0026#34;result\u0026#34;:\u0026#34;ACCEPT\u0026#34;} [trace] {\u0026#34;mode\u0026#34;:\u0026#34;plan_and_execute\u0026#34;,\u0026#34;step\u0026#34;:2,\u0026#34;decision_maker\u0026#34;:\u0026#34;executor\u0026#34;,\u0026#34;action\u0026#34;:\u0026#34;await_brief_approval\u0026#34;,\u0026#34;result\u0026#34;:\u0026#34;wait_marker_recorded\u0026#34;} [trace] {\u0026#34;mode\u0026#34;:\u0026#34;plan_and_execute\u0026#34;,\u0026#34;step\u0026#34;:3,\u0026#34;decision_maker\u0026#34;:\u0026#34;executor\u0026#34;,\u0026#34;action\u0026#34;:\u0026#34;write_draft\u0026#34;,\u0026#34;result\u0026#34;:\u0026#34;blocked_by_action_gateway\u0026#34;} [summary] {\u0026#34;mode\u0026#34;:\u0026#34;plan_and_execute\u0026#34;,\u0026#34;waiting_for_approval\u0026#34;:true,\u0026#34;draft_written\u0026#34;:false,\u0026#34;usage\u0026#34;:{\u0026#34;total_tokens\u0026#34;:142}} # Workflow：代码固定迁移，模型从未见过 write_draft [trace] {\u0026#34;mode\u0026#34;:\u0026#34;workflow\u0026#34;,\u0026#34;step\u0026#34;:1,\u0026#34;decision_maker\u0026#34;:\u0026#34;llm\u0026#34;,\u0026#34;event\u0026#34;:\u0026#34;generate_brief\u0026#34;,...} [trace] {\u0026#34;mode\u0026#34;:\u0026#34;workflow\u0026#34;,\u0026#34;step\u0026#34;:2,\u0026#34;decision_maker\u0026#34;:\u0026#34;workflow\u0026#34;,\u0026#34;action\u0026#34;:\u0026#34;WAITING_FOR_APPROVAL\u0026#34;,\u0026#34;result\u0026#34;:\u0026#34;run_suspended\u0026#34;} [summary] {\u0026#34;mode\u0026#34;:\u0026#34;workflow\u0026#34;,\u0026#34;waiting_for_approval\u0026#34;:true,\u0026#34;draft_written\u0026#34;:false,\u0026#34;usage\u0026#34;:{\u0026#34;total_tokens\u0026#34;:203}} 三段摆在一起，结论是确定的：\nReAct 能停吗？ 能，但停的条件由模型选中的 finish 或代码给的最大步数决定；“等待”不在它自动遵守的条件里。 Plan-and-Execute 能停吗？ 计划里写了 await_brief_approval，但朴素 Executor 把它当普通动作执行——只改状态、继续下一步，最后靠 Action Gateway 拦下 write_draft 的副作用，可 Executor 并没有因此停，它仍往下走到 finish。 Workflow 能停吗？ 当前节点生成内容后，代码把下一状态固定成 WAITING_FOR_APPROVAL 并结束 Run；模型连 write_draft 这个动作都没被暴露。 关键洞察：await 写在计划里是数据，只有 break / return / 状态机迁移 才是控制。指望模型或 Planner 在文本里“自觉地停下来”，就是把安全押在概率上。\n外层 Workflow，内层 ReAct 既然 Workflow 最可控，ReAct 最灵活，生产里怎么选？\n答案是分层，而不是二选一。一个可信的技术文章 Agent 可以长这样：\nWorkflow 固定阶段： 0. 准备材料 ──\u0026gt; 1. 生成纲领 ──\u0026gt; [等待审批] ──\u0026gt; 2. 生成梗概 ──\u0026gt; [等待审批] ──\u0026gt; 3. 写草稿 ^ | 探索节点（ReAct）：搜证据、读源码、判断“材料够不够”，直到确定性条件达成 外层用 Workflow 锁阶段和硬边界：未达到 WAITING_FOR_APPROVAL，后面的阶段就不存在，模型没机会跳到“写草稿”。 内层用 ReAct 降低不确定性：一个“读懂这个仓库再写梗概”的开放任务，让模型多走几步、看 Observation，比死板的一次性流程更实用。 任务清晰后再交给 Planner/Executor：计划进入 Executor 前先用确定性校验器检查结构、动作白名单和审批顺序；LLM 可以辅助语义判断，但硬规则必须由代码执行——校验器只返回 ACCEPT / REVISE / REJECT，不替模型悄悄改计划。 这里有个很容易被模糊的点：“能不能进计划”由谁判？\n不要让模型直接回一个 ready: true。那是模型自我声明，等于让它自己给自己批作业。正确的分工是：\nLLM 交结构化证据：已调研了哪几项、还剩哪几项没做、每项的依据是什么。 Runtime 按校验契约算布尔：条件满足才算 ready，证据缺失就补齐或换策略。 这就是“模型提建议，代码做决定”在 readiness 上的具体落法。\n关键洞察：给开放任务留 ReAct，给高风险边界上 Workflow，再把“能不能切换阶段”写成运行时契约；三者不是竞争关系，是一条流水线上的不同工位。\nWorkflow 该不该上框架 也许你已经想到 LangGraph 这类框架：它能不能把上面这些自动解决？\n能解决一部分，但要分清“能力”和“承诺”。LangGraph 的文档把两件事分开：Workflow 走预定义的代码路径，Agent 每一步由模型动态决定过程；它用状态图、checkpointer、interrupt() 提供挂起、保存状态和外部命令恢复的实现能力。这些能力正好是第十二话要做“可恢复、不可绕过的审批”的原料。\n但它不会因为你用了 LangGraph，就自动保证审批合规。状态图上的节点连得通，不代表业务上这一段就该执行。publish_article 这种迁移在不允许发布的状态下必须由运行时拒绝——框架给你的是“状态图能跑”，不给你“业务边界天然正确”。\n所以这一话只把 LangGraph 标记为实现方向，不展开 API。下一步要去的地方，就是把“接下来该做什么”从人工解读变成可以被恢复、被拦截、被审计的运行时机制。\n一句话收口：框架替你搬运状态，不替你决定业务。安全边界只能来自你写的校验契约和动作网关。\n下一话：Skill 只是提示词——如何让 Agent 真正停下来，并且跨进程可以恢复、无法绕过审批。 踩坑速查 症状 原因 解法 --live 报 LLM network error / DNS 解析失败 沙箱或本地网络到模型 API 不通 用离线 python3 -m unittest -v 先验证控制流；真实调用再解决网络/代理 模型返回值解析失败或 content 为空 部分推理模型默认 thinking，content 在 reasoning 里 显式 thinking:{\u0026quot;type\u0026quot;:\u0026quot;disabled\u0026quot;} 并要求 JSON 输出 Java 运行报 UnsupportedClassVersionError 用了旧版 JDK 用 JDK 21 + ./gradlew，或直接 javac/java 跑 MinimalSuspendDemo 计划里写了“等待”，运行时不暂停 把等待当成了数据标记 改成 return/break/状态机迁移，并写进回归测试 参考 LangGraph Documentation, “Workflows and Agents”, https://docs.langchain.com/oss/python/langgraph/workflows-agents/（Workflow 与 Agent 的路径决定方式，成文时已核对）。 LangGraph Documentation, “Interrupts / Human-in-the-loop”, https://docs.langchain.com/oss/python/langgraph/interrupts/（interrupt()+checkpointer 的挂起恢复能力，本篇只作方向标记）。 本话演示代码与回归测试：配套代码仓库中的 python/control_modes.py、java/.../Main.java、java/.../MinimalSuspendDemo.java。 ","date":"2026-09-01T00:00:00Z","image":"/images/gya-c11-agent-control-modes-cover.png","permalink":"/post/gya-c11-agent-control-modes/","title":"第十一话｜ReAct、Plan-and-Execute 还是 Workflow：Agent 的下一步由谁决定？"},{"content":"“这双鞋七天内能退吗？”\nNovaShop 的资料里没有这句话。资料写的是：“NovaTrail X2 签收后 7 个自然日内可申请退货；商品需保持完好且配件齐全。”\n两句话说的是同一件事，字面上却对不上。如果助手只会查关键词，这个问题很可能直接被漏掉——用户不会觉得是“没搜到”，只会觉得这个助手不懂人话。\n这一话就把这次匹配拆开，看三件事：问题怎样变成数字，三条政策怎样被排出先后，以及“排第一”到底意味着什么。\n先别急着谈向量 先准备三段极小的候选资料：\n标识 候选片段 return-policy NovaTrail X2 签收后 7 个自然日内可申请退货；商品需保持完好且配件齐全。 shipping-policy NovaTrail X2 标准配送通常在付款后 1 至 3 个工作日发货，偏远地区时效可能延长。 size-guide NovaTrail X2 提供黑色 M 码与 L 码；下单前请参考尺码表确认脚长。 如果只做关键词匹配，“七天内能退”里的“退”，和资料里的“申请退货”，靠的是同一个意思，而不是同一个词。想接住这种说法，传统做法是在词表里补同义词规则——把“退”“寄回去”“不合适”都映射到“退货”上。\n规则不是没有价值。SKU、订单号这类精确标识，恰恰适合这种方式：用户原样报出“NS-X2-BLK-M”，关键词匹配又快又准。\n可客服问题远不止 SKU 和订单号。用户会说“能退吗”“不喜欢能不能寄回去”“尺码不合适怎么办”，把每种说法都写进词表，维护成本很快失控。\nEmbedding 换了个思路：不比字符串，先把文本变成一串数字（向量），再比较两段文本在这个空间里离得近不近。阿里云百炼把 Embedding 定义为把文本等数据转换为数值向量，用于语义搜索等下游任务。官方文档\n系统比较的到底是什么 候选政策可以提前编码、保存。用户发来问题时，系统只需要编码这一个问题，再拿它和候选向量逐个比较。\n检索先找“该看哪几段”，回答环节再决定怎样组织答案。 这里有三件事不能混：\n查询和候选片段必须用同一个 Embedding 模型编码，否则两者未必落在同一套表示空间里。 参与比较的两条向量必须等长；维度不同，余弦相似度无从算起。 得到的只是当前候选集合里的排序，不是“答案正确率”。 这很像 Java 里的 Comparator：它能给对象排出先后，却不会替你判断“排第一的就是正确答案”。向量分数也一样——只负责排队，不负责下结论。\n密集检索的通行做法，就是分别编码问题和文本片段，再用相似度挑选候选上下文，DPR 论文用的正是这种双编码器思路。Karpukhin 等，2020 我们选余弦相似度，只是为了把计算摊开看，不代表所有检索系统都这么算。\n相似度怎么算 候选片段和查询都变成了向量，“近不近”就变成了数值比较。那这个数值具体怎么算？这一节用余弦相似度回答。\n$$ \\operatorname{cosine}(q,d)=\\frac{q \\cdot d}{\\lVert q \\rVert\\lVert d \\rVert} $$\nq 是查询向量，d 是一条候选片段向量。分子把两个向量逐维相乘再相加（点积），衡量它们在每个维度上是不是朝同一个方向使劲；分母乘上两个向量的长度，避免“向量长分数就高”。结果有直观的含义：同方向的向量分数接近 1，垂直的接近 0，反方向接近 -1。排序器关心的，就是谁离 1 更近。\n余弦公式有两个必须预先处理的边界：两条向量要同维、非零。维度不一致，逐维相乘根本对不上；零向量没有方向，分母一除就崩。排序器在这里主动报错而不是硬算，是为了让配置错误早点暴露，而不是让一个坏分数混进排序。\ndef cosine_similarity(left: Sequence[float], right: Sequence[float]) -\u0026gt; float: \u0026#34;\u0026#34;\u0026#34;计算两个同维、非零向量的余弦相似度。\u0026#34;\u0026#34;\u0026#34; if len(left) != len(right): raise ValueError(\u0026#34;向量维度不一致，不能计算余弦相似度\u0026#34;) # 点积衡量两个向量在各维度上的同向程度。 dot_product = sum(a * b for a, b in zip(left, right, strict=True)) left_norm = math.sqrt(sum(value * value for value in left)) right_norm = math.sqrt(sum(value * value for value in right)) # 零向量没有方向；继续计算会产生除零错误，也没有检索意义。 if left_norm == 0 or right_norm == 0: raise ValueError(\u0026#34;零向量没有方向，不能计算余弦相似度\u0026#34;) return dot_product / (left_norm * right_norm) 这一段代码和公式一一对应：先检查输入，再算点积，再算两个长度，最后相除。拒绝分支不是额外负担，它就是“余弦在不同输入上的定义边界”。\n只取前 K 名 三个候选都有分数了，接下来怎么挑？答案朴素：按分数从高到低排，取前 K 个。这就是 Top-K 里的“K”。\n排序里有一个容易忽略的小决策：分数并列时怎么办。rank_candidates 用原始下标做第二排序键，保证同一份输入反复运行得到同一份顺序——方便测试和复现，但不代表两个并列候选在业务上有高低。\ndef rank_candidates( query_vector: Vector, candidates: Sequence[Candidate], candidate_vectors: Sequence[Vector], *, top_k: int, ) -\u0026gt; list[RankedCandidate]: \u0026#34;\u0026#34;\u0026#34;按余弦相似度降序返回前 ``top_k`` 个候选。\u0026#34;\u0026#34;\u0026#34; if len(candidates) != len(candidate_vectors): raise ValueError(\u0026#34;候选片段数量与候选向量数量必须一致\u0026#34;) if top_k \u0026lt;= 0: raise ValueError(\u0026#34;top_k 必须大于 0\u0026#34;) ranked_with_index = [ (index, RankedCandidate(candidate, cosine_similarity(query_vector, vector))) for index, (candidate, vector) in enumerate(zip(candidates, candidate_vectors)) ] ranked_with_index.sort(key=lambda item: (-item[1].score, item[0])) return [item[1] for item in ranked_with_index[:top_k]] RankedCandidate 只是把候选片段和分数绑在一起的容器。真正的逻辑只有三步：逐条算分数、按分数降序排（分数打平看原始下标）、切片取前 top_k。\n用一个玩具例子看：查询向量 [1.0, 0.0]，三个候选向量是 [1.0, 0.0]、[2.0, 0.0]、[0.0, 1.0]，分数分别是 1.0、1.0、0.0。取 top_k=2，结果是前两个候选——同分时保住原始顺序，第三个出局。\n先把本地验证跑起来 排序逻辑不依赖云端，验证也先离线做。运行：\ncd /Users/renxin/ai_brain/GYR python3.11 -m unittest discover -s c02-embedding-topk/tests -v 测试喂的全是固定小向量，把排序器的不变量直接写进断言：同方向向量 [3.0, 4.0] 和 [6.0, 8.0] 的余弦必须是 1.0；并列分数的三个候选取前两名必须稳定得到 first、second。\nself.assertAlmostEqual(cosine_similarity([3.0, 4.0], [6.0, 8.0]), 1.0) results = rank_candidates( [1.0, 0.0], candidates, [[1.0, 0.0], [2.0, 0.0], [0.0, 1.0]], top_k=2, ) self.assertEqual([result.candidate.chunk_id for result in results], [\u0026#34;first\u0026#34;, \u0026#34;second\u0026#34;]) 这次实际运行 4/4 通过：同向向量、零向量拒绝、维度不一致拒绝、并列分数稳定排序。\n为什么坚持先离线验证？因为云端调用不可重放——每次可能在小数后几位漂移，还要消耗配额；而排序逻辑是确定性的，它该在本地一次测对。云端只负责一件事：把文本变成向量。\n接上真实模型 本地排序器验证完毕，现在把玩具向量换成百炼 Embedding 的真实输出。接云端要做三件事：设环境变量、跑 main.py、看输出。\n请求走的是 OpenAI 兼容接口的 /embeddings：把模型名和输入文本发过去，拿回一串浮点数。Base URL 和 API Key 从环境变量读取，不要写进代码、文章或 Git。\nexport DASHSCOPE_API_KEY=\u0026#39;你的密钥\u0026#39; export DASHSCOPE_COMPATIBLE_BASE_URL=\u0026#39;https://你的业务空间域名/compatible-mode/v1\u0026#39; python3.11 c02-embedding-topk/main.py --top-k 3 这次真实运行（模型 qwen3.7-text-embedding，请求 1024 维、实际返回 1024 维）的输出是这个形状，以第一个问题为例（候选正文为排版截断，分数与排序未改）：\nmodel=qwen3.7-text-embedding requested_dimension=1024 actual_dimension=1024 query=这双鞋七天内能退吗？ 1. return-policy\tscore=0.650210\tNovaTrail X2 签收后 7 个自然日… 2. shipping-policy\tscore=0.445476\tNovaTrail X2 标准配送通常在付款后… 3. size-guide\tscore=0.418887\tNovaTrail X2 提供黑色 M 码与 L 码… 模型、地域和业务空间配置都可能变化，接入时以你的控制台和官方接口说明为准。\n这里有一个容易忽略的边界。main.py 把“候选片段”和“用户问题”分成两批请求，代码里保留了 document 与 query 两种角色，这是为了对齐百炼“检索任务建议区分查询和文档”的惯例；但我们这次用的是 OpenAI 兼容模式，接口里没有对应的角色字段——本地变量叫 query，不代表服务端真的按“查询模式”去编码。\n跑出来的结果：直觉对了，边界也露出来了 三个问题都跑完，Top-1 汇总如下：\n用户提问 Top-1 候选 分数 能说明什么 这双鞋七天内能退吗？ return-policy 0.650210 这次口语化退货问题把退货片段排在第一。 下单后多久能发货？ shipping-policy 0.614013 换成物流问题后，同一候选集的第一名随之变化。 NS-X2-BLK-M 什么时候发货？ shipping-policy 0.686929 “发货”语义占了主导；不能据此证明 SKU 精确检索可靠。 顺带说明：同一模型、同一候选集，分数会在第四位小数附近轻微漂移，这正是“分数不能当阈值”的一部分。以上面这次运行为准。\n第三行最容易让人误判。“NS-X2-BLK-M 什么时候发货”确实把“发货政策”排到了第一，看着像答对了方向；可候选资料里根本没有这件 SKU 的发货记录。这个结果只能说明“发货”两个字在语义上占了上风，证明不了 SKU 检索可用。\n分数能做什么、不能做什么 开篇承诺的三件事，到这里都能回答了：问题是怎样变成数字的（Embedding），三条政策是怎样排出先后的（余弦相似度 + Top-K），排第一意味着什么——它意味着“在当前候选集合里最靠前”，仅此而已。\nTop-K 的作用很朴素：把后续要看的资料缩小到几段。至于分数本身，别指望它替你判断下面这些事：\n不能直接据此判断 原因 最终答案正确 回答仍可能误读、遗漏或编造候选内容。 证据足够 高分只表示相对靠前，不代表覆盖了回答所需条件。 可以忽略精确词 SKU、订单号、政策版本等字段常需要过滤或关键词能力补充。 一个分数阈值通吃 分数依赖模型、候选集合、切分方式和相似度定义。 一句话收束：排第一不等于答案可信。 0.65 只说明“退货片段排在最前”，不说明“这条退货规则就是答案”。所以 Agent 的检索工具到这里就该停下——它交回候选证据和来源，而不是把分数包装成“答案可信”。之后的回答、过滤、引用、拒答，仍然是编排层要承担的事。\n结尾：一整篇政策，只有一个分数 前面用到的候选都是精心切成一段的小文本。如果候选本身就是一整篇长政策呢？我们把退货政策扩成六个条款：\nNovaTrail X2 退货政策：签收后 7 个自然日内可申请退货；商品需保持未使用状态、包装完整，并保留全部配件与购买凭证；因商品质量问题退货时，运费由 NovaShop 承担，退款将在收货后 3 个工作日内原路退回；因个人原因（尺码、喜好等）退货时，运费由买家承担；特价清仓商品与定制商品不支持无理由退货；运输途中损坏的商品可以申请换货。\n问它：“鞋收到是坏的，退货运费谁出？”（环境变量沿用上一节，运行 main_long_policy.py）\n这次的真实结果：\n-- 整篇政策作为一个候选（整篇只有一个分数）-- 1. return-policy-full\tscore=0.585282\tNovaTrail X2 退货政策：… -- 同一篇政策拆成条款逐条打分（预告第三话的切分视角）-- 1. clause-4\tscore=0.708807\t因个人原因（尺码、喜好等）退货时，运费由买家承担； 2. clause-3\tscore=0.697954\t因商品质量问题退货时，运费由 NovaShop 承担，退款将在收货后 3 个工作日内原路退回； 3. clause-6\tscore=0.675624\t运输途中损坏的商品可以申请换货。 整篇政策只有一个分数 0.585282——我们只知道“这篇政策相关”，却不知道是哪个条款把它顶上去的。拆成条款后，三条与“运费/损坏”相关的条款全部浮到前面，定位明显变细。\n这里还有一层诚实的噪声：排第一的是条款 4（“个人原因，买家承担”），而不是条款 3（“质量问题，NovaShop 承担”）。“坏的”按语义应该指向质量问题，但“运费谁出”的表层表达把条款 4 顶了上来。也就是说：分块让“哪一段相关”变得可见，但并没有让排序变成正确——检索找到候选是一回事，候选能不能支撑回答是另一回事，那要等后面的章节处理。\n第二话到这里，检索单元的粒度问题就摆上台面了：一整篇政策只算一个候选片段，粒度还是太粗。长文档到底该切成多大一块，才能既找得到规则，又不丢掉规则的上下文？\n参考资料 阿里云百炼：向量化（Embedding） 阿里云百炼：通用文本向量同步接口 API 详情 Dense Passage Retrieval for Open-Domain Question Answering ","date":"2026-08-30T00:00:00Z","image":"/images/gyr-c02-embedding-topk-cover.png","permalink":"/post/gyr-c02-embedding-topk/","title":"第二话｜怎么根据用户的提问找到相关的退货政策？"},{"content":"前面把 Agent 的循环、状态、记忆和工具接起来之后，用户仍可能面对一个黑盒。\n他发出“帮我检查这个仓库的测试为什么失败”，页面却在几十秒内没有任何变化。Agent 也许正在生成，也许正在调用工具，也许已经失败；用户无从分辨。\n问题出在传输方式上：普通 HTTP 是一问一答——客户端发一个请求，服务端算完最后一次性返回完整响应。Agent 干活要几十秒，响应就空白几十秒。要让 Agent 不再像黑盒，前端不只需要最终答案，还要持续接收文本增量、工具状态和 Run 的终态。\nSSE（Server-Sent Events）就是把这类服务端到浏览器的连续事件放进 HTTP 的一种标准方式。结论先说：SSE 不负责让 Agent 变聪明；它负责把 Agent 已经发生的事，按事件边界可靠地呈现给 UI。\n一、旧方案为什么看不见 “给我一个答案”用普通 HTTP 正合适：提交命令，取回结果，一次交互结束。但“让我看着你干活”不行——服务端在没有完整结果前不会返回响应，浏览器也就拿不到任何进度。\n要让用户看见进度，服务端必须在计算完成前就开始输出，并且持续输出。这正是 SSE 站在 HTTP 上的理由：一个响应保持打开，服务端不断写入“有边界的小段文本”。\n这里有个经常被混在一起的概念要拆开说清楚。HTTP 持久连接（persistent connection / Keep-Alive）指的是底层连接可以复用、承载多次请求响应，它降低建连成本，但服务端不会在没有新请求时主动推送业务消息。RFC 9112 §9.3 长轮询是应用层写法：服务端先不立即返回，等到有数据或超时才结束响应，浏览器收到后再发起下一次请求。它们都不等于“一个持续打开、服务端主动下推的响应”。\n四种常用方式的层次差别，一张表就能看明白：\n方式 它解决什么 通信方向 一次交互是否持续 Agent 中合适的职责 普通 HTTP 提交命令、取得一次结果 双向，但一问一答 否 创建 Run、提交用户输入、取消任务、确认敏感工具调用 HTTP 持久连接 复用底层连接，减少建连成本 仍是多次 HTTP 请求/响应 否 通常由客户端、网关和连接池处理，不是事件通道方案 SSE 服务端持续发送有边界的文本事件 服务端 → 浏览器 是 文本增量、工具进度、Run 状态、最终结果 WebSocket 持续的双向消息 双向 是 协同编辑、高频客户端指令、双方都需随时发消息的交互 WebSocket 与 HTTP 的关系也要说准确：它用 HTTP Upgrade 完成开场握手，握手成功后双方交换的是 WebSocket 帧，而不是继续交换 HTTP 响应体。RFC 6455 §4\n关键洞察：持久连接是连接复用策略；SSE 是 HTTP 上的事件流格式；WebSocket 是经 HTTP 握手切换出的双向协议。它们不在同一抽象层。\n二、亲眼看：SSE 在线上传了什么 浏览器用 EventSource 发起请求，服务端以 Content-Type: text/event-stream 返回响应，并持续写入按行组织的 UTF-8 文本。每个事件以空行结束。WHATWG HTML Standard §9.2\n下面是一个完整事件，不是一段“随便输出的文本”：\nid: 42 event: text.delta data: {\u0026#34;runId\u0026#34;:\u0026#34;run_123\u0026#34;,\u0026#34;delta\u0026#34;:\u0026#34;正在读取测试报告…\u0026#34;} 四个常用字段的职责如下。\n字段 协议层含义 在 Agent 流中的用法 data 事件数据；可以多行，浏览器会按规范合并 放 JSON payload，例如文本增量或工具结果摘要 event 事件类型；省略时是默认 message 区分 text.delta、tool.started、run.completed id 更新浏览器保存的最后事件 ID 用作断线续传的游标；服务端仍需校验 Run 归属 retry 建议浏览器下一次重连前等待的毫秒数 仅调整重连等待，不解决事件去重或业务恢复 注意最后一行的空行不是装饰。它是事件边界：如果连接在空行前结束，尚未完成的事件不会被分发。WHATWG HTML Standard §9.2.5–9.2.6\n浏览器端不需要自行解析每一行：\nconst source = new EventSource(`/runs/${runId}/events`); source.addEventListener(\u0026#34;text.delta\u0026#34;, (event) =\u0026gt; { const { delta } = JSON.parse(event.data); appendAssistantText(delta); }); source.addEventListener(\u0026#34;run.completed\u0026#34;, () =\u0026gt; { source.close(); }); EventSource 对断开连接具备重连处理，并在已有事件 ID 时把 Last-Event-ID 带回服务端。它提供的是恢复的协议钩子，不是“消息恰好一次送达”的保证；应用仍要定义续传、去重和终态行为。WHATWG HTML Standard §9.2.3–9.2.4\n三、事件的形状由业务决定：Agent Run 事件契约 只把模型 token 一段段传到 UI，用户会看到“它在说话”，但仍不知道它是否在行动。\n对一个会调用工具的 Agent，更有用的做法是把 Run 生命周期投影成一组应用层事件：\n例如，“检查测试为什么失败”这个 Run 在客户端看来可以是这样一串事件：\nevent: text.delta data: {\u0026#34;runId\u0026#34;:\u0026#34;run_123\u0026#34;,\u0026#34;delta\u0026#34;:\u0026#34;我先运行测试。\u0026#34;} event: tool.started data: {\u0026#34;runId\u0026#34;:\u0026#34;run_123\u0026#34;,\u0026#34;tool\u0026#34;:\u0026#34;run_tests\u0026#34;,\u0026#34;callId\u0026#34;:\u0026#34;call_7\u0026#34;} event: tool.completed data: {\u0026#34;runId\u0026#34;:\u0026#34;run_123\u0026#34;,\u0026#34;callId\u0026#34;:\u0026#34;call_7\u0026#34;,\u0026#34;summary\u0026#34;:\u0026#34;3 个测试失败\u0026#34;} event: run.completed data: {\u0026#34;runId\u0026#34;:\u0026#34;run_123\u0026#34;} 这些名称不是 SSE 规定的字段值，而是本文建议的业务事件契约。SSE 只规定如何分帧、如何指定事件类型、如何传递最后事件 ID；tool.started 是否存在、数据结构是什么、是否向用户展示工具结果，都由你的 Agent 产品和安全策略决定。\n这和 Java 微服务中的“领域事件”和“Kafka 的传输记录”很像：Kafka 不替你定义订单状态机；SSE 也不替你定义 Agent Run 状态机。先定义业务状态和权限边界，再选择传输通道。\n四、动手：跑通一条本地 Agent Run 事件流 这一节的 demo 不调用真实模型，避免让 API Key、模型费用或网络问题遮住协议本身。服务端固定模拟一个 Run：它先发送文本增量，再报告工具开始、工具完成，最后发送 Run 完成事件。\n配套代码有两份等价实现：Python 位于 GYA 的 c16-sse-agent-stream/，Java 位于 GYA-Java 的同名 Gradle 子模块。正文以 Python 为主，Java 版用于验证同一协议不依赖语言或 Spring 框架。\nPython：标准库即可启动 前置环境：Python 3.9+ 与 curl，无第三方依赖、无 API Key。\n终端一启动服务：\ngit clone git@github.com:renxin2024/GYA.git cd GYA/c16-sse-agent-stream python3 main.py --once 终端二订阅：\ncurl -N http://127.0.0.1:8765/events 我在本机实际收到的输出如下：\nid: 1 event: text.delta data: {\u0026#34;runId\u0026#34;:\u0026#34;run_demo\u0026#34;,\u0026#34;delta\u0026#34;:\u0026#34;我先运行测试。\u0026#34;} id: 2 event: tool.started data: {\u0026#34;runId\u0026#34;:\u0026#34;run_demo\u0026#34;,\u0026#34;tool\u0026#34;:\u0026#34;run_tests\u0026#34;,\u0026#34;callId\u0026#34;:\u0026#34;call_1\u0026#34;} id: 3 event: tool.completed data: {\u0026#34;runId\u0026#34;:\u0026#34;run_demo\u0026#34;,\u0026#34;callId\u0026#34;:\u0026#34;call_1\u0026#34;,\u0026#34;summary\u0026#34;:\u0026#34;3 个测试失败\u0026#34;} id: 4 event: run.completed data: {\u0026#34;runId\u0026#34;:\u0026#34;run_demo\u0026#34;} 这里最重要的不是 sleep(0.1)，而是每次写完一帧后立刻 flush，并且用最后那个空行完成事件。把空行删掉，再用浏览器 EventSource 接收时，读者会发现它不会把这一帧当作完成的事件。\nJava：同一事件流，不换协议 前置环境：JDK 21 与 curl。GYA-Java 使用仓库自带的 Gradle Wrapper；首次运行会按仓库配置下载 Gradle 和 Jackson。\n终端一启动：\ngit clone git@github.com:renxin2024/GYA-Java.git cd GYA-Java ./gradlew :c16-sse-agent-stream:run --args=\u0026#34;--once\u0026#34; 终端二订阅：\ncurl -N http://127.0.0.1:8766/events Java 版构建通过，并按相同顺序输出 text.delta、tool.started、tool.completed、run.completed。核心的分帧代码和 Python 同构，只是把字符串拼接换成了 Jackson 序列化 payload：\nprivate static byte[] encode(Event event) throws IOException { // SSE 是 UTF-8 的按行协议。最后的空行是事件边界；删除它会让 // EventSource 持续等待，而不是把前面的字段作为一条完整事件分发出去。 String frame = \u0026#34;id: \u0026#34; + event.id() + \u0026#34;\\n\u0026#34; + \u0026#34;event: \u0026#34; + event.type() + \u0026#34;\\n\u0026#34; + \u0026#34;data: \u0026#34; + JSON.writeValueAsString(event.data()) + \u0026#34;\\n\\n\u0026#34;; return frame.getBytes(StandardCharsets.UTF_8); } Event 是一个只保存 id、type、data 的 record；EVENTS 里和 Python 一样放着四个固定事件。既然协议的“形状”完全由分帧代码决定，换语言、换框架都不会改变这条事件流的语义——这正是把协议层与业务层分开的价值。\n如果 Java 版本因 toolchain 无法启动，先运行 java -version 确认实际环境是 JDK 21。这个 demo 故意保持 Java 21，不为了迁就本机默认 JDK 而降低系列约束。另外，不要把 Python 与 Java 输出的 JSON 键顺序当作正确性标准：JSON 字段顺序不属于 SSE 契约。\n五、能连通 ≠ 体验到好：三个坑 连得上、有输出，不代表体验是对的。下面是三个最常见的“能连通、但体验很差”的问题。\n1. 服务端在发，用户却最后一次性看到 SSE 需要端到端按事件及时传输。规范明确提醒：不恰当的块缓冲可能延迟事件分发。WHATWG HTML Standard §9.2.5\n因此排查顺序不该从“模型为什么这么慢”开始，而应沿着浏览器 → CDN/网关 → 反向代理 → 应用服务器逐跳确认：响应类型是否正确、是否有缓冲、应用是否真的在事件产生时写出。curl 先关掉客户端缓冲（curl -N）；如果 curl 能一帧帧出来、浏览器不行，问题大概率在中间层。具体关闭何种缓冲取决于网关产品和部署方式，不能从一段通用配置照抄。\n2. 断线后重复显示，或者漏掉关键状态 浏览器会携带 Last-Event-ID 重连，但服务端必须决定这个 ID 的语义：它是某个 Run 内单调递增的序号，还是全局事件 ID？事件保留多久？补发窗口过期时如何回退？\n一个可维护的最小约定是：runId + eventId 唯一；客户端按该组合去重；服务端只允许已授权的主体按 runId 查询或续传。这里的授权检查不能因为客户端带了一个事件 ID 就被跳过。\n3. Run 已结束，连接和资源还在等待 run.completed、run.failed、run.cancelled 这类终态应有清晰语义：服务端发送终态事件后结束流，客户端收到后关闭 EventSource 并更新界面。心跳注释可以帮助发现中间链路的空闲超时，但心跳不应伪装成业务进度。MDN SSE 指南\n关键洞察：SSE 解决“事件怎样流到浏览器”；事件是否可恢复、可授权、可解释，仍是 Agent Run 模型的责任。\n六、回到开场：黑盒变成了可见的事件流 现在回头看开头的那个 Run。“帮我检查这个仓库的测试为什么失败”在 demo 里变成了一条可见的时间线：先是“我先运行测试。”的文本增量，然后是工具开始、工具完成（3 个测试失败），最后 Run 完成。用户不再对着空白页面猜 Agent 是不是卡死了——他能看到 Agent 正在做什么、做到哪了、结果如何。\n这就是这一话真正改变的东西：“它在动”从结论变成了过程。 SSE 不负责让 Agent 变聪明，它负责把 Agent 正在做和已经做的事，按事件边界可靠地送到浏览器；之后要不要展示工具结果、要不要拒绝敏感调用，仍然是产品与安全决策。\n七、选型：什么时候选 SSE，什么时候不选 如果 UI 的主要需求是“服务端持续告诉用户发生了什么”，SSE 是很自然的第一候选：浏览器有原生 EventSource，协议有事件类型和重连语义，接入普通 HTTP 基础设施的成本通常较低。\n如果客户端也需要高频、低延迟地主动发消息，例如多人协同编辑、实时控制面板或双向交互游戏，再评估 WebSocket。若只是提交用户问题、取消 Run 或同意执行高风险工具，普通 HTTP 往往更直观，也更容易把请求、权限校验和审计记录放在清晰的命令边界上。\n不要用“WebSocket 更实时”替代选型分析。对 Agent 来说，最关键的问题是：消息主要朝哪个方向流？是否需要浏览器原生重连与事件 ID？你的 Run 是否有可恢复的状态模型？\n八、下一步 这一话放在 GYA 的生产级补充位置：它不增加 Agent 的推理能力，却决定用户能否看见、理解和恢复一次正在运行的 Agent 任务。\n从开场那个 Run 继续往后走，下一步应把同样的事件契约接到真实的 Run 状态存储、鉴权与可观测性中——而不是继续往 SSE 帧里塞业务判断。\n参考资料 WHATWG HTML Standard — Server-sent events RFC 9112 — HTTP/1.1 Persistence RFC 6455 — The WebSocket Protocol MDN — Using server-sent events Spring Framework — MVC SSE Spring Framework — WebFlux return values ","date":"2026-08-28T00:00:00Z","permalink":"/post/sse-in-ai-agent-development/","title":"第十六话｜SSE 是什么？为什么它会用在 AI Agent 开发中"},{"content":"一笔钱来得比预期快，通常先带来的是兴奋。\n接着，它会带来一个很少被认真处理的问题：我真的有能力承接它吗？\n这里的“承接”不是道德判断。不是说赚得多的人就该克制，也不是说敢于尝试的人必然冒进。\n它只是一个很朴素的事实：获得一笔钱的机制，和让它长期不伤害生活的机制，不是同一套。\n一次项目奖金、一次行业上行、一次副业爆发，甚至一次偶然的资产收益，都可能把人带到原先没有经历过的决策规模。钱变多了，决策复杂度也随之变多：税务、现金流、家庭责任、合同、杠杆、合伙、流动性，以及那个最容易被忽略的问题——失败后还能不能退回来。\n这篇文章不讨论某个公众人物是否做对了什么，也不试图解释任何人的成败。它只讨论一个更有普遍性的问题：当机会收入出现后，怎样不把“我赚到了”误读成“我什么都懂了”。\n赚到与守住，至少是四份不同的工作 人们常把财富视为一个结果，好像账户余额增长就代表所有能力一起增长。\n但如果把它拆开，会发现至少有四份不同的工作。\n能力 它真正要回答的问题 容易出现的误判 获得收入 我为什么能赚到这笔钱？ 把时点、环境和协作全部归因为个人能力 留住现金流 收入暂时中断时，生活还能维持多久？ 把账面资产当成随时可用的现金 运营资产 新业务、房产或项目能否持续运转？ 把购买或投资决定当作经营能力 承担尾部风险 最坏情况发生时，损失是否可承受？ 只算上行，不设计退出边界 这四项能力彼此相关，但不会自动迁移。\n一个人可以非常擅长写代码、谈客户、做内容，或者抓住一个增长机会；这说明他在某个环节建立了优势。可当问题变成“要不要开一家公司”“要不要把大部分现金押进一个流动性很差的东西”“能不能为别人做担保”时，面对的已经是另一组变量。\n这很像一个后端服务的职责边界。一个 API 返回 200，只能证明这一条调用路径此刻成功；它不能证明数据库备份、缓存击穿、限流策略、监控告警和故障回滚都已准备好。\n收入也是如此。一次收入成立，只证明收入机制在某个时间点成立。\n关键洞察：会赚钱不是总能力的总开关；它只是一个已经被验证的局部能力。\n有了意外收入，人不会做出同一种“理性反应” 我们很喜欢用单一故事解释财富变化：有人挥霍，所以留不住；有人克制，所以守得住。\n现实没有这么整齐。\n加拿大央行的一篇工作论文研究了家庭面对意外收入时的消费与储蓄选择。论文的建模背景与 2008 年美国经济刺激付款有关，讨论的是流动性、意外收入规模和规划期限如何影响反应。它呈现的并不是一条“拿到钱就应该怎样做”的规则，而是一个更有用的提醒：不同约束条件下，家庭的选择会显著不同。论文原文\n这项研究不能直接给中国读者开财务处方，也不能用来判断某个个案。但它足以纠正一个直觉：意外收入之后的选择，并不是一个只由“人够不够聪明”决定的问题。\n流动性是否充足、已有责任有多重、未来收入是否稳定、有没有时间理解新领域、是否存在可信的专业支持——这些条件，会共同决定一笔钱是变成缓冲，还是变成新的脆弱点。\n下面的判断是本文的推论，而不是论文结论：\n真正需要升级的，往往不是一个人的自信，而是他的决策系统。\n当资源规模扩大而系统没有扩容，过去靠直觉就能处理的选择，会突然变成高耦合问题。你以为是在做一次消费、一次投资或一次创业，实际上可能同时改变了家庭现金流、负债期限、职业选择和人际关系。\n三次最常见的能力外推 1. 从单点成功，外推到全局判断 一次成功很容易制造一种错觉：既然我在 A 上判断对了，那么 B、C、D 大概也差不多。\n但专业能力、经营能力、资产定价能力与谈判能力，分别需要不同的信息和反馈周期。写出一个好产品，不等于能控制固定成本；认识一个靠谱的人，不等于理解合同风险；某次投资盈利，也不等于收益的来源可持续。\n区分这两句话很重要：\n这次结果是对的； 这个过程可复制。 前者可能只需要一次机会，后者需要多轮验证。\n在工程里，我们不会因为一次压测通过，就宣布系统已经具备无限扩展性。面对收入机会，也该保留同样的克制：先问收益从哪里来、依赖什么条件、条件变化后还剩下什么。\n2. 从账面增长，外推到可持续现金流 账面资产、可动用现金、未来收入和负债责任，常常被混在一起讨论。\n但它们承担的职责完全不同。账面上升不等于能及时变现；能变现不等于不会折价；每月收入高不等于在收入中断时仍有安全垫。\n因此，判断一个决策能不能承受，不能只问“值不值钱”，还要问：\n现金什么时候到账，什么时候必须支出？ 如果需要用钱，多久能拿到，可能付出什么代价？ 收入暂时归零后，已有责任会不会继续增长？ 这不是悲观，而是避免把不同时间尺度的资源放进同一个数字里。\n关键洞察：账面上的富裕，和时间表上的安全，是两件事。\n3. 从可以试错，外推到可以承受失败 “我可以试试”本身没有问题。\n问题在于，很多人把“我有一笔钱可以尝试”翻译成“我可以承担这次失败的一切后果”。\n二者的差别，藏在失败后的选择权里。一次损失如果只影响一个小的试验账户，你仍然可以调整方向；如果它侵蚀的是家庭基本支出、职业转换窗口或下一次学习的成本，那它就不再是普通试错。\n风险管理不是保证不亏，而是确保一次亏损不会把你逼到只能做更差决策的位置。\n这和线上系统的故障域设计很像。不是假设服务永远不会出错，而是让一个组件的失败不要拖垮所有组件。\n把“守住”做成一个决策系统 守住并不等于什么都不做，更不等于把机会全部错过。\n它更接近于：在行动之前，把不同性质的资金和风险分开，让一次判断失误仍然处于可恢复范围内。\n面对大额消费、跨界创业或高风险资产决策前，可以先做一次四问检查。\n第一问：现金流还撑得住吗？ 假设新增收入突然归零，既有生活、家庭责任与刚性支出能维持多久？\n这个问题不是要给出一个放之四海而皆准的月数，而是要把“我感觉还可以”转化成自己的现金流表。\n第二问：最坏损失是否被隔离？ 这次决定最坏会损失多少？这部分损失是否会碰到生活底线、应急储备或他人的基本权益？\n如果答案不清楚，先不要用热情替代边界。\n第三问：退出路径存在吗？ 如果判断错了，能否停止、转让、缩减，或者让损失自然到期？\n没有退出路径的决策，往往会诱发持续追加：不是因为新证据支持，而是因为人不愿承认已经投入的成本。\n第四问：信息从哪里来？ 关键判断来自可验证的数据、独立专业意见与书面约束，还是来自熟人背书、群体情绪与“别人都在做”？\n信息来源越单一，越应该降低下注规模。不是因为所有人都不可信，而是因为你的判断系统需要能被反证的输入。\n这四问并不替代律师、会计师、持牌财务顾问等专业意见；当决策涉及合同、税务、借贷或他人资金时，专业边界反而更重要。它们的作用只是先把一个模糊的冲动，变成一组可以核查的条件。\n最后：要守住的，是机会消失后的选择权 人当然可能赚到暂时超出既有经验范围的钱。\n这并不羞耻，也不奇怪。时代、行业、协作与运气本来就会把人推到新的位置。\n真正值得补上的，不是“我配不配”的自我审判，而是与新资源相匹配的系统：把一部分收益变成缓冲，把一部分不确定性变成边界，把一部分兴奋留给学习成本。\n这样做并不能保证永远成功。\n但它会让一次机会即使消失，你仍保有调整、学习和重新开始的选择权。\n而这，才是“守住”最有价值的含义。\n参考资料 Michael Boutros, Windfall Income Shocks with Finite Planning Horizons, Bank of Canada Staff Working Paper 2022-40, 2022. ","date":"2026-08-22T00:00:00Z","image":"/images/cong-zhuan-dao-shou-zhu-cover-v2.png","permalink":"/post/cong-zhuan-dao-shou-zhu/","title":"从赚到到守住：机会收入之后，如何保住选择权"},{"content":" “NovaTrail X2 支持 7 天无理由退货吗？”\n把这个问题交给一个裸大模型，它大概率会给出一段很像客服的话：商品需要保持未使用、包装完整，并保留购买凭证。\n听起来没什么问题。\n但它不知道 NovaShop 的真实退货政策。它没有看过这份商品资料，不知道规则适用于哪个地区，也不知道政策是不是昨天刚刚改过。\n它只是生成了一段听起来合理的文字。\n这就是 RAG 系列要解决的起点：大模型虽然会聊天，但它不知道你的退货政策。\n一、裸大模型：会生成答案，但没有企业知识 先不谈 RAG，看看最原始的系统是什么样：\n用户问题 → Chat 模型 → 文本回答 这类模型可以翻译、总结、写代码，也能把客服话术写得很自然。但它的回答依赖两类输入：训练时学到的参数知识，以及本次请求中传入的上下文。\n企业自己的商品政策、内部流程、最新价格和租户数据，通常不在它能够直接访问的上下文里。模型并不会因为“你是 NovaShop 客服”这句话，就自动获得 NovaShop 的资料。\n所以裸模型的第一个边界很清楚：它可以生成企业知识问答的语言，却没有企业知识问答的证据。\n二、第一种补救：把资料塞进 Prompt 最简单的办法，是把政策直接放进提示词：\n你是 NovaShop 客服。 以下是退货政策： NovaTrail X2 支持签收后 7 天内无理由退货，商品须未使用、包装完整，并保留配件和购买凭证。 请根据这段政策回答用户的问题。 这个办法确实有效。模型现在能看到资料，也能根据资料生成回答。\n问题是，企业资料不会永远只有一段。商品规格、物流规则、保修政策、不同地区的退货政策和新旧版本都会继续增加。最后，Prompt 会变成一份越来越长、越来越难维护的知识文档。\n当回答出错时，排查也很困难：资料没有放进去？放进去了但没有检索到？检索到了但模型没有使用？还是引用了已经过期的版本？\nPrompt 方案解决的是“把资料放到模型眼前”，却没有解决“资料如何管理、如何查找、如何证明回答有依据”。\n关键洞察： Prompt 可以临时装下知识，但不能替代一条可维护、可追溯的知识链路。\n三、第二种补救：先检索，再生成 RAG 在 Prompt 方案前面增加了一步：模型回答之前，系统先去资料中找相关内容。\n用户问题 ↓ 检索相关资料 ↓ 组装上下文 ↓ Chat 模型生成回答 ↓ 返回引用或拒答 这条链路可以拆成五个动作：\n动作 要回答的问题 文档准备 企业资料从哪里来？ 检索 哪些片段和当前问题有关？ 上下文组装 哪些内容真正交给模型？ 生成 模型是否只根据这些内容回答？ 引用与拒答 能否指出依据，证据不足时能否不回答？ RAG 原始论文把外部检索到的知识接入生成过程，用来缓解参数知识难以更新、访问和追溯的问题。Lewis 等，Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks\n这里还要先纠正一个常见误解：RAG 不等于向量数据库。 Retriever 可以使用关键词检索、Elasticsearch 的分词器和倒排索引、向量检索、结构化查询，也可以把几种方式组合成混合检索。本系列会在后续文章里分别比较这些方式；本篇只把“检索”当作一个黑盒步骤，不展开它的内部算法。Elasticsearch，Search approaches\n四、从最小 RAG 到企业级知识助手 RAG 让模型“回答前先查资料”，但这还只是企业知识助手的中间一层。真正的系统需要沿着两条链路运行。\n1. 文档入库链路 企业资料不是凭空出现在检索器里的，它要先经过入库：\n文档上传 → 解析与清洗 → 分块 → 建立索引 → 保存版本和来源 这一条链路解决的是“系统手里到底有什么资料”。文档解析决定哪些内容能被看见，分块决定检索的最小单位，索引决定怎么找到它们，版本和来源决定答案能不能回溯。\n2. 问答链路 用户提问时，系统再走另一条链路：\n用户问题 → 检索候选片段 → 权限与版本过滤 → 组装上下文 → 模型回答 → 引用、拒答与记录 两条链路合起来，才是企业级 RAG 的基本轮廓：\n%%{init: {'theme':'neutral'}}%% flowchart LR A[\"企业文档\"] --\u003e B[\"解析与索引\"] B --\u003e C[\"检索\"] D[\"用户问题\"] --\u003e C C --\u003e E[\"上下文与模型\"] E --\u003e F[\"回答与引用\"]这张图只画了主链路，没有画出所有生产组件。评测、权限、版本、任务恢复和观测会横向影响多个节点，后面分别展开。\n五、跑一次全链路预览 理论地图有了，接着跑最小实现。配套代码目前保存在私有工作区的 code/agent-tutorial/c01-rag-overview/，尚未作为公开代码仓库发布；实验只依赖 Python 3.10+ 标准库。云端运行需要百炼兼容模式的 DASHSCOPE_API_KEY。\ncd code/agent-tutorial/c01-rag-overview export DASHSCOPE_API_KEY=你的Key python3 main.py 本次预览实验使用阿里云百炼：\n角色 模型 Chat deepseek-v4-flash-0731 Embedding qwen3.7-text-embedding 没有 Key 时，可以先验证本地链路：\npython3 main.py --offline python3 -m unittest discover -s tests -v 离线模式只验证这条链路的数据结构、上下文组装和引用格式，不代表云端模型的回答效果。云端跑通的判定标准是：看到模型配置、命中的来源、上下文字符数、回答，以及形如 [source:returns-cn#chunk-001] 的引用。至于系统为什么会把某些片段排在前面，下一篇再拆开解释。\n2026-08-21 的一次真实运行结果如下：\n{\u0026#34;mode\u0026#34;:\u0026#34;dashscope\u0026#34;,\u0026#34;provider\u0026#34;:\u0026#34;aliyun-bailian\u0026#34;,\u0026#34;chunks\u0026#34;:3,\u0026#34;chat_model\u0026#34;:\u0026#34;deepseek-v4-flash-0731\u0026#34;,\u0026#34;embedding_model\u0026#34;:\u0026#34;qwen3.7-text-embedding\u0026#34;} {\u0026#34;question\u0026#34;:\u0026#34;NovaTrail X2 是否支持 7 天无理由退货？\u0026#34;,\u0026#34;sources\u0026#34;:[\u0026#34;returns-cn#chunk-001\u0026#34;,\u0026#34;novatrail-x2#chunk-001\u0026#34;],\u0026#34;context_chars\u0026#34;:215,\u0026#34;answer\u0026#34;:\u0026#34;支持。根据退货政策，NovaTrail X2 可在签收后 7 天内无理由退货，但需满足商品未使用、包装完整、保留配件和购买凭证等条件。 [source:returns-cn#chunk-001]\u0026#34;} {\u0026#34;question\u0026#34;:\u0026#34;NovaShop 是否提供月球基地配送？\u0026#34;,\u0026#34;sources\u0026#34;:[\u0026#34;returns-cn#chunk-001\u0026#34;,\u0026#34;novatrail-x2#chunk-001\u0026#34;],\u0026#34;context_chars\u0026#34;:215,\u0026#34;answer\u0026#34;:\u0026#34;无法确认。现有资料中未提及月球基地配送服务。\u0026#34;} 上面是从真实输出中保留本篇关心字段后的结果。第一个问题走通了完整链路：系统返回了相关来源，模型根据上下文回答，并附上了来源标记。至于这些来源如何被计算和排序，本篇先不展开。\n第二个问题暴露了当前 Demo 的边界。资料里没有月球基地配送，但系统还是返回了两个候选片段，因为程序现在固定返回若干候选。模型这次回答“无法确认”，但无关片段已经进入上下文；换一个问题，模型可能会被这些噪声影响。\n这说明“能检索、能生成”还不等于“知识助手可靠”。我们还需要阈值、评测、引用校验和拒答策略。\n如果运行失败，先看错误发生在哪一层：DASHSCOPE_API_KEY is not set 表示当前 Shell 没有读到 Key；401 或 403 通常是 Key 无效、过期或没有权限；404 model not found 则要检查模型名、地域和百炼工作空间。代码默认使用 aliyun-bailian、deepseek-v4-flash-0731 和 qwen3.7-text-embedding，也可以通过 README 中的环境变量覆盖。\n关键洞察： 这个 Demo 的价值不是证明 RAG 已经可靠，而是把“资料从哪里来、系统找了什么、模型看到了什么、回答引用了什么”第一次暴露出来。下一篇，我们再追问“系统为什么找到了这些片段”。\n六、GYR 系列接下来会补齐什么 现在可以回头看整个系列的路线。每一篇不是凭空增加一个组件，而是在解决上一层暴露出来的具体问题：\n阶段 要解决的问题 主题 第一话 整条链路到底是什么 RAG 全景与演进路线 第二话 问题和文本为什么可以比较 Embedding、余弦相似度、Top-K 第三话 一篇长文应该如何检索 文档分块与 Chunk 设计 第四话 找到资料后如何可靠回答 上下文、引用与拒答 第五话–第六话 如何保存和评价检索结果 PGVector、版本与评测基线 第七话–第八话 关键词和向量如何配合 检索优化与混合检索 第九话–第十二话 企业资料如何进入系统并保持边界 解析、租户权限、任务恢复与观测 第十三话–第十四话 如何收敛为可运行的服务 API、模块化原型与框架对照 这条路线有一个刻意的约束：先手写最小机制，再引入数据库、服务和框架。否则读者很容易得到一个“能启动的 RAG”，却不知道每个组件为什么存在。\n下一篇：Embedding 到底做了什么 现在我们知道 RAG 的整体链路，也看到一个最小版本确实可以把问题和资料交给模型。但“检索”仍然像一个黑盒：为什么退货政策排在前面？相似度分数是什么？SKU 这种精确编号也适合这样检索吗？\n下一篇只回答一个问题：一句用户问题，为什么可以和一段商品政策计算相似度？\n我们会检查 Embedding 的真实输出，手写余弦相似度和 Top-K 排序，把第一话图里的“检索”拆开来看。\n第一篇只需要记住一句话：\nRAG 是裸大模型走向企业知识助手的第一条外部知识链路，但它远没有替企业完成评测、权限和数据治理。\n参考资料 Patrick Lewis 等，Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks，2020。 Alibaba Cloud Model Studio，List models。 Alibaba Cloud Model Studio，DeepSeek API。 Elasticsearch，Search approaches。 ","date":"2026-08-21T00:00:00Z","image":"/images/gyr-c01-enterprise-rag-start-cover.png","permalink":"/post/gyr-c01-enterprise-rag-start/","title":"第一话｜大模型会聊天，但怎么让它知道你的退货政策？"},{"content":" 你把同样的问题发给 AI，得到了两种截然不同的待遇。\n第一个问题：「我怎样才能赚大钱？」AI 秒回——副业清单、投资组合、技能提升，十二条建议整整齐齐。你收藏了，然后呢？什么也没发生。\n第二个问题：「为什么赚钱是我当前的目标？我真正想通过钱获得的是什么？」AI 停了一下，开始一层层地剥：你怕什么、你要什么、你拿什么换。这一次对话变成了镜子。\n同一个 AI，两种结果。差别不在模型，在问题本身。\n答案是廉价的，问题不是。\n价值正在从「答案」转移到「问题」 先给结论：AI 让答案变得便宜之后，人的竞争力被迫从「拥有答案」转向「提出好问题」。\n过去几千年，答案一直是稀缺品。知识锁在书里、锁在专家脑子里，谁掌握更多答案，谁就站在食物链上游——医生、律师、工程师，都是答案的持有者。那时「知道得多」就是竞争力，因为你多知道一个答案，就可能少走一条弯路。\nflowchart LR A[多年积累] --\u003e B[拥有答案] B --\u003e C[竞争力]现在这条链路断了。AI 以近零边际成本生产答案：训练一次，复制无限份，一分钟生成一份分析、一份方案、一段代码。答案的供给曲线被人为拉平——你曾经要用十年换的东西，现在一秒到手。\n竞争点被迫上移：\nflowchart LR A[AI 供给答案] --\u003e B[答案变廉价] B --\u003e C[竞争点上移到提问] C --\u003e D[提问能力 = 新竞争力]这不是预测，是正在发生的事：当所有人都能拿到答案，「知道什么」不再分出高下，「不知道该问什么」和「能问出什么」开始真正分出高下。李飞飞在 Huberman 访谈里说过一句值得琢磨的话——苏格拉底是顶尖的「提示词工程师」（li-feifei-huberman 访谈）。\n而苏格拉底这套本事，2400 年前就被人完整记录下来，方法可复制。\n怎么问：苏格拉底式追问三步法 色诺芬的《回忆苏格拉底》本质上是苏格拉底的一份辩护记录——不是替自己辩护，而是替「提问」辩护：那些著名的对话里，苏格拉底几乎没给出过答案，他只是不断追问，直到对方发现自己站不住（Memorabilia，Xenophon）。\n这套追问有固定模式，三步：\nflowchart LR A[① 询问定义] --\u003e B[② 寻找例外] --\u003e C[③ 重新定义] --\u003e D[逼近真相]第一步：询问定义。 不满足于模糊概念，逼对方给出确切定义。「你说正义？正义是什么？说具体一点。」\n第二步：寻找例外。 拿你的定义去撞现实，找一个它解释不了的案例。「你说正义就是诚实？那骗敌人呢？骗生病的友人呢？这也是正义的缺失吗？」\n第三步：重新定义。 例外击穿了旧定义，逼出一个更精确的新定义，然后循环。\n《回忆苏格拉底》里，这套路几乎对所有人都跑通了：\n对藏书家欧绪德谟：他收藏所有名家的书，自认为智慧超群。苏格拉底先定下规则：说谎、欺骗、使坏，全归入「不义」。然后抛出反例——将军骗敌人、掠夺敌人的城，是正义还是不义？欧绪德谟被迫承认这些都是正义。他退一步说：「那对朋友撒这些谎总归不义吧？」苏格拉底又问：父亲把药说成食物，骗生病的孩子吃下去，算不算说谎、该归哪栏？朋友抑郁想自杀，你偷走他的刀，又归哪栏？欧绪德谟节节败退，最后承认「对朋友，直来直去也未必总对」——他最初那个「正义=诚实」的定义，一个回合都撑不住（Memorabilia 4.2.14-18）。 对享乐主义者阿里斯提普斯：他说「人生要在统治与奴役之间走一条中间道路，那是自由，是通往幸福的大道」。苏格拉底反驳：路是有的，但你要先退出这个世界；只要你还活在世上，「强者有的是办法让弱者吃苦头」——种下的庄稼被抢收，砍下的树木被人搬走。自由不是回避选择，是有能力不被欲望控制；你宣称的「岁月静好」，其实是把决定权交给了别人（Memorabilia 2.1.11-13）。 对想从政的格老孔：一个不到二十岁、立志当城邦领袖的年轻人。苏格拉底一连串问题：「城邦收入从哪里来？总额多少？军力如何？防御如何？」格老孔答不上来——他连城邦的基本盘都没研究过，就急着统治它（Memorabilia 3.6）。 三个案例，同一个动作：不给答案，只检查对方的概念是否清晰、逻辑是否一致、假设是否成立。\n今天把这三步搬到 AI 面前，就是提示词工程的内核。注意，不是「更长的提示词」——是更清醒的提问者。\n提问的边界：为什么你唤醒不了所有人 到这里，有个重要的问题必须被问出来：既然提问这么有力量，为什么苏格拉底自己，最后被处死了？\n柏拉图《理想国》第七卷的洞穴寓言讲得很明白（Republic 514a-520a）：\n一群囚徒从小被锁在洞穴里，脸朝岩壁，只能看见火光投射的影子，他们以为影子就是全世界。有一个人挣脱锁链，走出洞穴，看见了真实的太阳。他回来，想告诉同伴——同伴的反应不是感激，是觉得他疯了。柏拉图写道：如果可能，他们会亲手杀死这个试图释放他们的人——\u0026ldquo;would they not kill him?\u0026quot;（Republic 517a，后人注释认为此处暗指苏格拉底的命运）。\n苏格拉底的结局是这则寓言的注脚：公元前 399 年，雅典人以「不敬神、腐蚀青年」的罪名审判他，判了死刑。他有盟友安排越狱，他拒绝了——按柏拉图记载，他从容饮下毒堇汁（Socrates，Wikipedia；《毒堇之杯》，Bettany Hughes, 2010）。\n提问为什么没救他？因为提问只对愿意反思的人有效。洞穴里的囚徒不想出去——他们不觉得那是牢笼，那是他们熟悉的世界。对不想照镜子的人，镜子就是冒犯。\n这不是教你放弃，是教你分清对象：\n对愿意反思的人（客户、读者、同行、AI）——提问是你的杠杆，三步法值得反复用 对不想反思的人——省下力气。你在洞穴里喊得越大声，越像那个「回来被当成疯子」的人 判断「对方愿不愿意反思」，本身就是提问能力的一部分——这才是完整的元技能。\n把 AI 当成一个没有方向感的天才学生 所以你手里现在有个前所未有的工具：一台永远在线、知识面无限、但没有方向感的机器。它像极了一个聪明的学生——什么都能答，就是不知道你真正想问什么。\n人和 AI 的协作，不是「我问你答」的一问一答，而是迭代逼近：\n提问 → 回答 → 发现漏洞 → 修正问题 → 再问 → 逼近你要的东西 操作上，对应三步法：\n先问自己再问 AI：拿到任何问题，先定义——我要解决的确切问题是什么？「怎么赚钱」不是问题，是焦虑；「我手里有什么可交付的东西能换钱」才是。 用例外去撞答案：AI 给它的第一版方案，主动找反例去撞它。「这个方案对我这种单兵作战的人成立吗？」 让它重新定义：把对话从「要答案」改成「逼它把隐含假设摆到台面上」——「你推荐这个方案，默认了我哪些前提？」 答案会越来越便宜，问题会越来越贵。当整个世界的答案供给趋近无限，你的提问能力，就是最后的分水岭。\n（本文史实与引文：色诺芬《回忆苏格拉底》2.1、3.6、4.2 英文原典（Perseus 数字库）；柏拉图《理想国》第七卷 514a-520a、517a（Perseus）；苏格拉底生平与审判见 Wikipedia「Socrates」；《毒堇之杯》为 Bettany Hughes 2010 年著作；李飞飞观点出自 Huberman Lab 访谈，knowledge-base li-feifei-huberman 条目。）\n","date":"2026-08-17T00:00:00Z","image":"/images/asking-beats-answering-cover-v2.png","permalink":"/post/asking-beats-answering/","title":"答案变廉价之后，提问成为最稀缺的能力"},{"content":"凌晨一点，你又刷到一条 AI 新闻：新模型发布，跑分屠榜；新框架上线，GitHub 一天涨了几千星。你点开教程，收藏，然后开始焦虑——上个月的还没学会，这个月又来了。这种日子什么时候是个头？\n先给结论：追不上不是你的问题，是知识生产方式变了。\nAI 以机器速度生产知识，人的追赶在物理上不可能。所以短期技术焦虑没有意义——不是\u0026quot;想开点\u0026quot;的安慰，是策略判断：把精力放在人能做的地方，才是唯一来得及的事。\n知识生产方式变了 过去几千年，知识由人生产，增长速度和人的学习能力同频。牛顿之后两百多年，一个受过训练的物理学家可以读完自己领域的全部重要文献，站到前沿。那时\u0026quot;追赶\u0026quot;是成立的：学完前人的，你就站在了最前面。\n现在不是了。\nAI 生产知识的边际成本趋近于零：训练一次，复制无限份；同一套推理能力，一分钟生成一篇分析、一份代码、一套方案。而人的学习带宽是常数——一天 24 小时，一年 365 天。一条指数曲线对一条水平线，剪刀差只会越拉越大。\n这不是我的情绪化判断。AI 研究界早就用自己的历史验证过这件事：Rich Sutton 在《The Bitter Lesson》里总结了 AI 研究 70 年最大的教训——利用算力的通用方法，最终以巨大优势碾压人类知识。\u0026ldquo;时间花在一边，就不是花在另一边\u0026rdquo;，人类知识路径与算力路径在实践中互斥，而后者赢了（Rich Sutton, \u0026ldquo;The Bitter Lesson\u0026rdquo;, 2019）。\ngraph LR A[\"知识生产方式\"] --\u003e B[\"过去：人类生产线性增长\"] A --\u003e C[\"现在：AI 生产指数增长\"] B --\u003e D[\"学完前人知识就能站到前沿\"] C --\u003e E[\"前沿以机器速度远离人类\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#d1fae5,stroke:#059669,color:#064e3b style C fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style D fill:#d1fae5,stroke:#059669,color:#064e3b style E fill:#fee2e2,stroke:#dc2626,color:#7f1d1d焦虑不是你的错，是机制 如果追不上是必然的，为什么我们还这么焦虑？因为焦虑有自己的运行机制，而且它自我强化。\n心理学上把 AI 焦虑定义为\u0026quot;预期性认知威胁反应\u0026quot;——对 AI 系统可能威胁自己职业地位、认知能力的持续性担忧。问题在于：我们的注意力和记忆系统天然偏向威胁内容。AI 进步的新闻、职业被替代的故事，比\u0026quot;AI 的平凡失败和局限\u0026quot;更容易被记住。于是循环成立：越焦虑，越关注 AI 信息；越关注，焦虑的素材越多（Leapwit《AI焦虑全面解读》）。\ngraph LR A[\"刷到 AI 新进展\"] --\u003e B[\"感觉落后于时代\"] B --\u003e C[\"焦虑\"] C --\u003e D[\"更拼命刷 AI 信息\"] D --\u003e B style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#fef3c7,stroke:#d97706,color:#78350f style C fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style D fill:#fef3c7,stroke:#d97706,color:#78350f还有一个时间尺度的错配：AI 能力的跃迁以月、年为单位——DeepSeek 从 V3 到 R1 只隔了不到一个月（2024-12-26 → 2025-01-20，官方发布记录）；而人类职业能力的培养周期以十年计。工业文明建立起来的\u0026quot;学得会\u0026quot;的线性逻辑，在摩尔定律式的迭代节奏面前被瓦解了（知乎专栏《如何理性看待\u0026quot;AI焦虑\u0026quot;》）。\n认清这个机制，焦虑就卸掉了一半：不是你不努力，是这套机制设计出来就是让你停不下来的。\n追新是失败策略 那继续追呢？继续追，等于主动跳进这个循环，而且追新的性价比还在持续恶化。\n一个被验证过的数据：Work AI Institute 对 6000 名数字工作者调查发现，受访者声称 AI 平均每周帮他们省下 11 小时，但只有 13% 的人报告公司绩效有改善。省下的时间去哪了？等待 AI Agent 完成任务（有人叫它\u0026quot;botsitting\u0026quot;）、在多个 AI 工具之间来回切换试答案、以及一种被称为\u0026quot;职场表演\u0026quot;的活动——表面上很忙，实际没在干活（Cal Newport, \u0026ldquo;AI Isn\u0026rsquo;t Breaking Work. It\u0026rsquo;s Already Broken\u0026rdquo;, 2026，引 Financial Times 报道）。\n工具层面尚且如此，知识层面更残酷：追新 = 消费别人的节奏。 你的学习时间被 AI 的发布节奏劫持，永远在别人划定的赛道上跑。更糟的是，追到的\u0026quot;新\u0026quot;半衰期越来越短——今天学的框架，明天可能是昨天模型的最优解。投入产出比一路恶化，焦虑却没有减少，因为追新的终点永远是下一个\u0026quot;新\u0026quot;。\ngraph LR A[\"拼命学新框架\"] --\u003e B[\"新工具/模型出现\"] B --\u003e C[\"刚学的东西贬值\"] C --\u003e D[\"更焦虑、追更狠\"] D --\u003e A style A fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#fef3c7,stroke:#d97706,color:#78350f style D fill:#fee2e2,stroke:#dc2626,color:#7f1d1d精力该放哪：人能做的地方 放下追新，不是躺平。是把有限的带宽，投到 AI 追不上你的地方。这些地方存在吗？存在，而且比想象中更清晰。\n判断。 哈佛公共卫生学院的一篇文章说得很直接：收集证据、比较选项正在变成自动化任务，但\u0026quot;决定什么合适、负责、明智\u0026quot;仍依赖人（Harvard Chan School, \u0026ldquo;Reclaiming Judgment\u0026rdquo;）。AI 可以给你十种方案，但\u0026quot;这件事该不该做、做到什么程度、对谁负责\u0026quot;——这是判断，不是计算。\n品味。 Paul Graham 2002 年就写过《Taste for Makers》，2026 年 2 月他重提这篇，说\u0026quot;在 AI 时代，品味会变得更加重要\u0026quot;。品味不是偏好——偏好可以被数据建模，亚马逊知道你爱买什么；品味是经验在神经系统里的缓慢校准，只能通过亲身经历、犯错、数年积累形成，没法从描述里获得（The Long Becoming, \u0026ldquo;Taste — The One Thing AI Cannot Have\u0026rdquo;）。AI 可以生成一万种方案，但\u0026quot;哪个好、为什么好\u0026quot;的直觉，是身体里长出来的。\n实践。 知识只有经过真实世界的摩擦才会被修正。把学到的东西尽快用起来，理论的不完整之处才会暴露——这正是\u0026quot;危险的自学者\u0026quot;四条原则之一（存在与刀锋，四个日本自学原则）。AI 没有肉身，不知道真实环境里什么会崩；你有。\n连接与创造。 Goodreads 创始人 Otis Chandler 的判断是：AI 擅长大规模组织信息，但判断质量、原创性、什么会长期重要，仍需要人的品味和判断（Otis Chandler 访谈, 2026）。读者的信任、客户的托付、伙伴的默契——这些连接没法被\u0026quot;生成\u0026quot;。\ngraph LR A[\"人能做的地方\"] --\u003e B[\"判断决定什么重要、对谁负责\"] A --\u003e C[\"品味经验校准出的直觉\"] A --\u003e D[\"实践真实世界的验证\"] A --\u003e E[\"连接与创造信任、原创、责任\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#d1fae5,stroke:#059669,color:#064e3b style C fill:#d1fae5,stroke:#059669,color:#064e3b style D fill:#d1fae5,stroke:#059669,color:#064e3b style E fill:#d1fae5,stroke:#059669,color:#064e3b指导自己：五条行动 这些话是说给自己听的。我 34 岁，后端出身，正在 AI 转型，比谁都容易掉进\u0026quot;追新\u0026quot;的坑。给自己定五条，也分享给你：\n停止刷新闻当学习。 信息获取 ≠ 认知积累。刷一百条 AI 新闻，不如想清楚一个问题。新闻让 AI 消化，人只读结论。 让 AI 干\u0026quot;知识搬运\u0026quot;的活。 摘要、索引、翻译、代码脚手架——交给 AI。省下来的时间做 AI 做不了的事。 省下的时间投入写作与实践。 写作是思考工具，不是记录工具；实践让知识经受真实世界的检验。这两个都是\u0026quot;危险的自学者\u0026quot;的核心原则。 以\u0026quot;完成作品\u0026quot;衡量成长，而不是\u0026quot;知道名词\u0026quot;。 一篇文章、一个项目、一次复盘——作品是判断力和品味的实体化。Cal Newport 的\u0026quot;慢生产力\u0026quot;说的就是这个：少而精，痴迷质量。 每份输出都沉淀进自己的系统。 不是收藏夹，是能检索、能复用、能演进的体系。日积月累的复利，才是长期主义真正兑现的地方。 Paul Graham 在《How to Do Great Work》里还有一个比喻：知识是分形扩展的，从远处看边缘平滑，走近才发现满是缝隙。你的任务不是追前沿，是找到属于自己的那条缝隙，把它挖深。 别人追的\u0026quot;新\u0026quot;，未必是你的\u0026quot;缝\u0026quot;。\n结尾 回到凌晨一点的场景。\n放下手机的那一刻，焦虑还在——但它不再是指令了。它只是提醒你：有一个算法在用你追不上的速度生产知识。你追不上，所有人都追不上。你能做的，是去写那篇一直想写的文章，去把那个拖了三个月的项目收尾，去和信任你的人多聊一次。\nAI 生产知识的速度，你改变不了。人能做的地方，你随时可以开始。\n关键洞察：短期技术焦虑的解药不是更努力地追，而是换一个竞争维度——从追赶 AI 生产的知识，转向积累 AI 无法复制的判断、品味、实践与连接。\n参考：Rich Sutton《The Bitter Lesson》(2019)；Cal Newport《AI Isn\u0026rsquo;t Breaking Work》(2026)；Leapwit《AI焦虑全面解读》(2026)；Harvard Chan School《Reclaiming Judgment》；Paul Graham《Taste for Makers》《How to Do Great Work》；The Long Becoming《Taste — The One Thing AI Cannot Have》(2026)；Otis Chandler 访谈 (2026)；知乎专栏《如何理性看待\u0026quot;AI焦虑\u0026quot;》。\n","date":"2026-08-17T00:00:00Z","image":"/images/letting-go-tech-anxiety-cover.png","permalink":"/post/letting-go-tech-anxiety/","title":"放下技术焦虑：AI 时代知识生产方式变了"},{"content":"你搭了一个知识库问答系统：上传文档、建好索引、跑通 demo——然后 LLM 开始答非所问。\n你以为是模型不够聪明。换更强的模型，还是不对。\n问题很可能出在一个不起眼的环节：文档入库时的分块。LLM 根本没读到该读的内容，模型再强也白搭。\n这篇文章把分块这件事讲透：为什么要分块、五种分块策略分别解决什么问题、生产环境到底怎么选。\n为什么要分块？三个绕不过去的原因 先把结论摆出来：解析完的文档，不能整篇扔进检索。\n原因有三个，每一个都独立成立。\n原因一：上下文窗口是有限的 LLM 的上下文窗口再大也有上限。就算现在有 128K、1M 上下文的模型，检索也不是把整个文档库塞进 Prompt——那样成本直接爆炸。\n更重要的是，窗口越大，模型越难从中找到准确的那一段。这就是著名的\u0026quot;大海捞针\u0026quot;问题：上下文拉长，检索精度反而下降。\n原因二：检索信噪比 用户问的是文档里的一个小问题，但整篇文档有几千字。\n整篇匹配会发生什么？不相关的内容把相关内容的信号稀释了。向量检索返回的 Top-K 结果里，噪音占了大多数，相关的那一小段被埋没。\n块太大 → 无关信息稀释信号。这是信噪比的第一面。\n原因三：Embedding 对长文本的\u0026quot;平均化\u0026quot; Embedding 模型擅长表示短文本——一两句话的语义最清晰。\n文本一长，向量就被\u0026quot;平均\u0026quot;了：整段的语义混在一起，变成一个模糊的方向。检索时，这个模糊向量既匹配不上问题，也匹配不上文档里的任何精确信息。\nEmbedding 模型对短文本的语义表示最好，长文本会被\u0026quot;平均\u0026quot;成模糊向量。\n三个原因叠加，结论只有一个：必须把文档切成合适大小的块。\ngraph LR A[\"① 上下文窗口有限\"] --\u003e D[\"必须分块\"] B[\"② 检索信噪比\"] --\u003e D C[\"③ Embedding 平均化\"] --\u003e D style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style D fill:#d1fae5,stroke:#059669,color:#064e3b但怎么切，才是真正的问题。切小了语境不够，切大了检索不精——这就是分块的核心矛盾：检索精度 vs 上下文完整性。\n分块的两个基础参数 在讨论策略之前，先理解所有分块策略都绕不开的两个参数。\n参数 含义 经验值（中文场景） chunk_size 每块的大小 300-800 字（约 500-1200 tokens） chunk_overlap 相邻块之间的重叠量 chunk_size 的 10%-20% 为什么需要 overlap？看一个例子。\ngraph LR A[\"文档AAAABBBBCCCCDDDD\"] --\u003e B[\"chunk_size=8, overlap=2块1: AAAABBBB块2: BBCCCCDDBB 被两块共享\"] A --\u003e C[\"chunk_size=8, overlap=0块1: AAAABBBB块2: CCCCDDBB-CC 之间语义断裂\"] B --\u003e D[\"✅ 跨块内容仍可检索\"] C --\u003e E[\"❌ 关键句恰好被切断时丢失\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#d1fae5,stroke:#059669,color:#064e3b style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#d1fae5,stroke:#059669,color:#064e3b style E fill:#fee2e2,stroke:#dc2626,color:#7f1d1doverlap=0 时，如果\u0026quot;BBCC\u0026quot;恰好是完整语义（比如一个概念的完整描述），它被拦腰切断，两块都不完整。检索时哪块都匹配不精准，LLM 拿到的都是残句。\noverlap 就是给跨块语义留的缓冲带。\n五种分块策略，逐个拆解 理解了参数，进入正题。分块策略从简单到复杂，正好是一条问题驱动的问题链。\n策略一：固定大小分块——最简单，也最粗暴 做法：按预设的字符数或 token 数直接切。\n# LangChain text_splitter = CharacterTextSplitter( chunk_size=500, chunk_overlap=50, ) 优点只有一个：实现最简单、速度最快。\n缺点却很致命：完全无视语义边界。可能在句子中间、甚至词语中间切断。\n切出来的块，很多是\u0026quot;半个意思\u0026quot;。检索时匹配到半句话，LLM 看着上下文猜——这正是答非所问的来源。\n适合日志、代码等结构弱、语义依赖不强的文本。自然语言文档用它，基本等于摆烂。\n策略二：递归分块——默认策略，性价比之王 做法：按优先级尝试分隔符，从大到小逐级切。\n# LangChain text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=[\u0026#34;\\n\\n\u0026#34;, \u0026#34;\\n\u0026#34;, \u0026#34;。\u0026#34;, \u0026#34;，\u0026#34;, \u0026#34; \u0026#34;, \u0026#34;\u0026#34;] ) graph LR A[\"超长段落\"] --\u003e B{\"按优先级逐级切分段落 → 换行 → 句子 → 词\"} B --\u003e|\"每块 ≤ 上限\"| C[\"✅ 在语义边界成块\"] B --\u003e|\"仍超限\"| D[\"降级到更细的分隔符\"] D --\u003e B style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#fef9c3,stroke:#ca8a04,color:#713f12 style C fill:#d1fae5,stroke:#059669,color:#064e3b style D fill:#fef9c3,stroke:#ca8a04,color:#713f12思路：优先在语义最完整的边界（段落、换行、句号）切断，实在超限才降级到更细的分隔符。\n优点：在保持语义边界和控制块大小之间取得最佳平衡，通用场景的默认选择。\n缺点：分隔符优先级需要按文档类型微调；对复杂结构文档（表格、代码）力不从心。\n策略三：语义分块——让 Embedding 参与决策 递归分块是按\u0026quot;形式\u0026quot;切（分隔符）。语义分块按\u0026quot;意思\u0026quot;切。\n做法：\n把文本拆成句子 计算每个句子的 Embedding 向量 计算相邻句子的语义相似度 在相似度\u0026quot;断崖\u0026quot;处切分 graph LR A[\"句子1: 聚类算法把样本分组\"] --\u003e B[\"句子2: K-Means 是经典算法\"] B --\u003e C[\"句子3: 明天北京多云转晴\"] C --\u003e D[\"语义断崖句子2→3 相似度骤降在这里切\"] A -.-\u003e|\"高相似\"| B B -.-\u003e|\"低相似\"| C style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style D fill:#fef9c3,stroke:#ca8a04,color:#713f12核心算法是新颖度检测：\nnovelty(s_i) = 1 - cosine_sim(embed(s_i), embed(context_window)) 当 novelty \u0026gt; 均值 + 0.8 * 标准差 → 切分 优点：语义连贯性最好，检索精度最高。这也是为什么生产环境在\u0026quot;疑难块\u0026quot;上会启用它。\n缺点：每个句子都要算一次 Embedding，计算成本翻倍。全量用不划算，适合作为精修手段。\n工具：semchunk、LlamaIndex SemanticSplitterNodeParser。官方 benchmark 里，semchunk 的 AI 分块模式在 Legal RAG QA 上正确率 37.7%，比 LangChain 递归分块的 34.8% 高约 2.9 个百分点。\n策略四：结构感知分块——利用文档自身的结构 如果文档本身有结构，为什么不直接用？\n做法：按文档的固有边界切——Markdown 按标题层级、HTML 按标签、代码按函数类定义、对话按轮次。\n关键技巧是注入结构元数据：每个 chunk 附带它在文档中的位置路径。\n{ \u0026#34;content\u0026#34;: \u0026#34;聚类算法的核心思想是...\u0026#34;, \u0026#34;metadata\u0026#34;: { \u0026#34;header_path\u0026#34;: \u0026#34;第三章 \u0026gt; 3.2 无监督学习 \u0026gt; 3.2.1 K-Means\u0026#34;, \u0026#34;section_level\u0026#34;: 3 } } 好处很实在：\n逻辑清晰，切块天然语义完整 检索结果自带上下文——LLM 不光看到这段内容，还知道它来自哪一章哪一节 graph LR A[\"第三章 无监督学习\"] --\u003e B[\"3.2 无监督学习\"] B --\u003e C[\"3.2.1 K-Meanschunk 携带 header_path\"] C --\u003e D[\"检索命中 3.2.1 时LLM 知道完整上下文位置\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#d1fae5,stroke:#059669,color:#064e3b style D fill:#d1fae5,stroke:#059669,color:#064e3b缺点：依赖文档格式规范。纯文本、扫描件这类无结构文档，它无从下手。\n策略五：父子分块——生产环境的首选 前面的策略都在解决一个问题：块大还是块小。父子分块跳出这个取舍——大小都要。\n做法：\n父块：段落/小节级别（500-1000 tokens），提供完整上下文 子块：句子级别（100-200 tokens），负责精确检索 检索流程：用小的找，用大的答。\ngraph LR A[\"用户提问\"] --\u003e B[\"在子块中检索句子级别，高精度\"] B --\u003e C[\"命中子块：K-Means 是经典算法\"] C --\u003e D[\"通过 parent_id 找到父块\"] D --\u003e E[\"返回父块完整内容上下文完整，LLM 能理解\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#d1fae5,stroke:#059669,color:#064e3b style C fill:#d1fae5,stroke:#059669,color:#064e3b style D fill:#fef9c3,stroke:#ca8a04,color:#713f12 style E fill:#d1fae5,stroke:#059669,color:#064e3bgraph LR subgraph P[\"父块：3.2.1 完整小节\"] C1[\"子块1: 算法定义\"] C2[\"子块2: 参数说明\"] C3[\"子块3: 适用场景\"] end Q[\"问题命中子块2\"] --\u003e C2 C2 --\u003e P P --\u003e LLM[\"LLM 收到父块全文\"] style P fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C1 fill:#d1fae5,stroke:#059669,color:#064e3b style C2 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style C3 fill:#d1fae5,stroke:#059669,color:#064e3b style Q fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style LLM fill:#d1fae5,stroke:#059669,color:#064e3b优点：检索精度与上下文完整性兼得——化解了分块最核心的矛盾。小粒度保证命中精准，大粒度保证 LLM 拿到完整语境，避免\u0026quot;找到了关键句但缺前因后果\u0026quot;。\n缺点：存储开销翻倍（父子块都要存向量）、实现复杂度较高。\n工具：LlamaIndex 的 auto-merging retriever / recursive retriever 就是这种思路的官方实现。\n生产环境怎么选 五种策略各有适用场景，先看总对比。\n策略 原理 优点 缺点 适用场景 固定大小 按字符/token 数硬切 最简单、最快 无视语义，可能在句中切断 日志、代码等弱语义文本 递归分块 按分隔符优先级切 语义边界与块大小平衡，通用 需调分隔符；复杂结构无力 通用默认 语义分块 用 Embedding 找语义断崖 语义最连贯、精度最高 计算成本翻倍 高质量问答、疑难块精修 结构感知 按文档标题/标签/轮次切 语义完整、自带上下文 依赖文档格式规范 Markdown/HTML/代码文档 父子分块 子块检索、父块作答 精度与上下文兼得 存储翻倍、实现复杂 生产环境首选 单看这张表，你可能会问：到底用哪个？\n答案是：生产环境用混合分块。单一策略覆盖不了所有文档类型——你的知识库里既有 Markdown 规范文档，也有扫描 PDF，还有代码片段。\ngraph LR B[\"① 结构感知粗切按标题/章节\"] --\u003e C{\"块大小判断\"} C --\u003e|\"过小\"| D[\"合并相邻块\"] C --\u003e|\"合适\"| E[\"直接使用\"] C --\u003e|\"过大\"| F[\"递归/语义细分\"] D --\u003e G[\"② 父子分块映射+ 元数据注入\"] E --\u003e G F --\u003e G style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#d1fae5,stroke:#059669,color:#064e3b style E fill:#d1fae5,stroke:#059669,color:#064e3b style F fill:#d1fae5,stroke:#059669,color:#064e3b style G fill:#dbeafe,stroke:#2563eb,color:#1e3a5f混合分块的思路：结构感知先粗切出框架，块大小判断决定后续处理（过小合并、合适直接用、过大递归/语义细分），最后用父子分块建立层级，注入元数据。\n落地建议 回到开头的场景——知识库答非所问，怎么排查？\n按这个顺序自查，90% 的问题能定位：\n先看分块：打开一个 chunk 看看，句子是不是被拦腰切断？上下文是不是残缺？ 默认从递归分块起步：chunk_size 500-800、overlap 10%-20%，把基线跑通 结构化文档换结构感知：你的文档有标题层级，就别浪费这个信息 检索精度不够再上父子分块：让子块找、父块答，通常能显著提升 疑难块用语义分块精修：只对检索差的高频问题块启用，控制成本 分块不是一个\u0026quot;选一次就完事\u0026quot;的环节。文档类型变了、检索效果变了，分块策略就要跟着调。这是 RAG 系统里最值得花时间的调优点之一——因为它是整个检索质量的起点，也是你最能控制的一环。\n本文基于个人对 RAG 工程实践的梳理与官方文档验证（MinerU OmniDocBench v1.6 得分 95.39 来自其官方 README；semchunk 基准数据来自其官方 README 及 isaacus.com 博客）。\n","date":"2026-08-13T00:00:00Z","image":"/images/rag-chunking-strategies-cover.png","permalink":"/post/rag-chunking-strategies/","title":"RAG 分块策略：为什么你的知识库答非所问，问题多半出在这里"},{"content":"你相信「努力就有回报」吗？\n这个问题放在二十年前，答案几乎是肯定的：好好读书 → 考好大学 → 找好工作 → 升职加薪。这是一条被反复验证的线性路径，一代人靠着它完成了阶层跃迁。\n但如果你出生在日本 1990 年代呢？\n泡沫破裂后，同样努力读书、同样考好大学、同样找工作的年轻人，等来的是「就业冰河期」——毕业即失业，或沦为低薪派遣工。「努力就有回报」这个公式，在存量社会里第一次大规模失效了。\n日本失去的三十年，不是一部失败史，而是一部个人职业的错题本。它提前演出了中国正在经历的结构性转型：当增长放缓、社会不再扩张，个人该怎么调整生存策略？\n这篇文章把日本经验拆开：增长社会的成功公式为什么失效、四类人是如何被时代抛弃的、以及普通人现在能做什么。\n增长社会的成功公式 先理解「增长社会」里个人成功的底层逻辑。\n一个经济体高速增长时，社会在持续扩张：新产业不断出现、岗位持续增加、蛋糕越做越大。在这样的环境里，个人的成功公式是线性升级：\n好学校 → 好工作 → 好生活 努力 → 升职 → 加薪 → 跃迁 这条公式之所以成立，靠的不是个人多聪明，而是社会总盘子一直在变大。你不需要抢别人的饭碗，因为新饭碗在不断产生。个人努力是「乘数」，社会增长是「基数」——基数一直在涨，乘数只要不为零，结果就在涨。\n这就是为什么过去二十年，家长拼命让孩子读书、考公、进大厂——他们相信的是这套线性逻辑。\n%%{init: {'theme':'base'}}%% graph LR A[\"好学校\"] --\u003e B[\"好工作\"] B --\u003e C[\"好生活\"] C --\u003e D[\"上升通道持续打开\"] D --\u003e A style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#d1fae5,stroke:#059669,color:#064e3b style D fill:#d1fae5,stroke:#059669,color:#064e3b存量社会：公式失效的三重打击 当经济进入存量阶段，社会不再扩张，这套公式开始失效。日本失去的三十年，给出了三个典型断面。\n第一重：就业冰河期——「努力就有回报」崩塌 1990 年代泡沫破裂后，日本企业大规模收缩招聘。原本「毕业即就业」的大学生，突然发现校招名额锐减，大量毕业生只能进入劳务派遣体系，做低薪临时工。\n这不仅是就业难，更是信念崩塌：年轻人按旧公式努力了二十年（应试地狱「四当五落」——睡四小时考上、睡五小时落榜），结果社会根本不给回报。「努力就有回报」在就业冰河期变成了「努力就有回报，但回报不属于你」。\n%%{init: {'theme':'base'}}%% graph LR A[\"增长社会：毕业 → 就业 → 升职\"] --\u003e C[\"同一套努力\"] B[\"存量社会：毕业 → 冰河期 → 派遣工\"] --\u003e C C --\u003e D[\"回报天差地别\"] style A fill:#d1fae5,stroke:#059669,color:#064e3b style B fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12第二重：考公周期——避风港变成风暴中心 当民间就业恶化，「稳定」成为稀缺品。日本公务员一度被捧上神坛——90 年代被称作「神的职业」，因为永不失业的稳定性价值飙升，年轻人挤破头去考公。\n但好景不长。财政紧缩后，公务员编制削减 25%、连续降薪，「神的职业」变成了「霞关的抹布」（霞关是日本官僚机构所在地，形容公务员像抹布一样被使唤）。如今报考率创历史新低，精英人才反而远离体制。\n一个完整的周期：民间好 → 公务员没人当 → 民间差 → 公务员成神坛 → 财政紧 → 公务员跌落 → 又没人当。避风港，也会变成风暴中心。\n第三重：学历贬值——平成三大崩溃之一 日本学者把平成时代总结为「三大崩溃」：金融崩溃、中产崩溃、学历崩溃。\n学历贬值的路径很清晰：校招市场崩塌 → 毕业即失业 → 企业废除校招 → 就业军备竞赛（大一就开始实习考证）→ 劳务派遣市场化 → 学历在劳务等级制度中的作用越来越微弱。最后大学生学费暴涨、回报骤降，甚至出现「低学历社会」——高中生直接放弃高考。\n教育，从「阶层跃迁的电梯」变成了「昂贵的自我安慰」。\n四类人的命运：线性逻辑的四种破产方式 如果把日本 30 年的失败经验拍成一部群像剧，主角是四类人，他们都按旧公式努力过，结局却各不相同：\n人群 他们的选择 结局 本质 大学生 按旧公式读书就业 就业冰河期，沦为低薪派遣工 被时代牺牲的一代 考公者 追求稳定，涌入体制 编制削减、降薪，「霞关的抹布」 避风港变风暴中心 高学历者 避难式考研，用学历拖延 硕士毕业发现产业已衰落，当低薪临时教师 把问题推迟 ≠ 解决问题 返乡者 从城市回乡村，靠政策岗位 财政断供、岗位消失，两头踏空 依赖外部输血，稳定性极差 四类人的共同点：都相信「只要选对了一条旧路径，努力就有回报」。但存量社会里，旧路径本身会失效——不是你不努力，是那条路已经不存在了。\n10 条启示，提炼成 3 个原则 《以日为鉴》的作者从日本 30 年经验里提炼了 10 条给普通人的启示。我把它们归纳为 3 个可执行的个人策略原则：\n原则一：识别周期 「稳定」和「风口」都可能是暂时的。\n公务员曾经是「人生失败组」（80 年代民间薪酬数倍于体制内），后来成了「神的职业」，再后来变成「霞关的抹布」。一个职业的「稳定」标签，本质上是经济周期的阶段性产物，不是职业本身的属性。\n个人策略含义：不要把「当下的稳定」当作「永久的稳定」。选职业时，问的不是「现在稳定吗」，而是「这个职业在什么周期、什么环境下稳定」。\n原则二：打造跨周期能力 「铁饭碗」不是某个职业，而是自己的能力。\n日本公务员也会「人少活多钱少」；高学历也会变成「穷忙族」；只有能力本身是跨周期的——能在不同经济环境下迁移、变现的能力，才是真正的铁饭碗。\n什么是跨周期能力？书中给的方向是：识别信号的能力（知道周期在哪）、选择细分赛道的能力（同是理工科，半导体和互联网天壤之别）、适应变化的能力（能从一种环境迁移到另一种）。\n原则三：找增量 当国内内卷时，海外是最大的机遇来源。\n日本唯一没有沉沦的领域是出海——产业、供应链、文化一步步走出去。对个人同样成立：当所有人挤在同一条存量赛道上时，增量在别处。\n书里特别强调：出海的最大机遇，不在为母国大公司打工，而在参与当地供应链建设、抓住本土化窗口。\n%%{init: {'theme':'base'}}%% graph LR A[\"存量社会生存法则\"] --\u003e B[\"识别周期稳定和风口都是暂时的\"] A --\u003e C[\"跨周期能力铁饭碗是能力不是职业\"] A --\u003e D[\"找增量内卷时海外是机遇\"] style A fill:#d1fae5,stroke:#059669,color:#064e3b style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style D fill:#dbeafe,stroke:#2563eb,color:#1e3a5f现在能做什么 回到开篇的问题：你相信「努力就有回报」吗？\n存量社会的答案不是「不相信努力」，而是努力的方向要变：\n停止把「稳定」当目标——稳定是周期的产物，不是你的护城河。把时间花在「离开这个平台还能不能活」上 每年问自己一次：我的能力在什么周期有效？ 如果它绑定在某个行业的扩张期，就要开始准备第二条能力曲线 关注增量，哪怕很小——一个新技能、一个细分方向、一个海外市场，都是从存量里撕开的口子 不要用学历/考公「推迟问题」——推迟不会解决问题，只会让你在更晚的时间点、更被动的状态下面对它 日本的经验不是预言中国一定重演，而是一面镜子：它让我们提前看到，当「增长」不再是默认选项时，个人靠什么活着。答案从来不是某个职业、某张文凭，而是你自己识别周期、适应变化、找到增量的能力。\n本文基于分析师 Boden《以日为鉴：衰退时代生存指南》（开明出版社，2025）阅读笔记的梳理，结合 Wikipedia「Lost Decades」词条与已发布《县城正在消失》的交叉参照。\n","date":"2026-08-13T00:00:00Z","image":"/images/survival-rules-in-stock-society-cover.png","permalink":"/post/survival-rules-in-stock-society/","title":"存量社会生存法则：从日本失去的三十年看个人策略"},{"content":"你有没有遇到过这种情况——\n一个典型的 Java Web 服务，Tomcat 默认线程池 200。请求处理逻辑很简单：查一下数据库，调一个远程接口，返回结果。每个请求从进入线程池到返回，大部分时间都在等数据库查询、等远程响应。\n并发一上来，问题就出现了：200 个线程很快全部阻塞在 IO 等待上，新请求全部排队。\n这时候你多半会想：线程池不够，加线程不就行了？\n试了才知道这条路走不通。加到 1000，确实多撑了一会儿——但 1000 个线程照样会阻塞在 IO 上，第 1001 个请求照样排队。想靠加线程支撑真正的并发量？几万并发需要几万线程，每个线程 1MB 栈，就是几十 GB 内存；就算内存扛得住，操作系统调度几万个线程的开销也会把 CPU 吃光。\n于是你看到一种奇怪的状况：CPU 大多是空闲的——因为线程都在等 IO，没有计算在跑；内存也没满——因为当前只有 200 个线程在跑，只占了 200MB 栈。但并发就是上不去，而且加线程这条路本身是死路。\n这不是硬件问题，是线程模型问题：线程是稀缺的「重」资源，一个线程被 IO 阻塞时，它什么都没干，却把一个宝贵的线程占住了。而 JDK 21 的虚拟线程，就是来改这个的。\n这篇文章用一条问题链讲透虚拟线程：平台线程为什么不够用 → 虚拟线程怎么解决 → 它适合什么、不适合什么。\n%%{init: {'theme':'base'}}%% graph LR A[\"核心结论：虚拟线程让 Java 支持百万级并发\"] --\u003e B[\"平台线程为什么不够用\"] A --\u003e C[\"虚拟线程怎么解决\"] A --\u003e D[\"边界适合什么\"] style A fill:#d1fae5,stroke:#059669,color:#064e3b style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style D fill:#dbeafe,stroke:#2563eb,color:#1e3a5f平台线程的四个瓶颈 Java 的传统线程（平台线程）和操作系统线程是一一对应的：Java 代码每创建一个线程，JVM 就调用操作系统 API 创建一个 OS 线程。\n%%{init: {'theme':'base'}}%% graph LR A[\"Java 线程\"] --\u003e B[\"OS 线程\"] B --\u003e C[\"内核调度1:1 映射\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#fef9c3,stroke:#ca8a04,color:#713f12这种 1:1 模型在 Java 诞生时没问题，但放到现代高并发场景，四个瓶颈逐个暴露。\n瓶颈 表现 根源 创建销毁贵 线程创建销毁涉及内核态切换 要操作系统参与 内存占用大 每个线程默认分配 1MB 栈空间 JVM 为每个平台线程预留栈 上下文切换贵 保存恢复大量寄存器、栈状态，CPU 缓存失效 内核调度，涉及用户态/内核态切换 可用数量有限 一个进程通常只能创建数千个线程 内存和调度成本双重限制 一句话总结：平台线程是「重」资源，不能随便创建。所以 Java 应用被迫用线程池复用线程——但线程池也有上限，并发一高就排队。\n这就是为什么现代语言纷纷引入轻量线程：Go 的 goroutine、Kotlin 的协程。Java 直到 JDK 21 才正式端出虚拟线程（JEP 444，2023 年 9 月 19 日发布）。\n虚拟线程：用户态轻量级线程 虚拟线程和平台线程最大的区别：它不由操作系统管理，由 JVM 管理。\n维度 平台线程 虚拟线程 创建销毁 内核态，贵 用户态，JVM 完成，便宜 内存占用 默认 1MB 栈 几 KB 级 上下文切换 内核调度，保存大量状态 用户态，保存少量状态 可用数量 数千级 数百万级 数字对比很直观：平台线程 1MB 栈，虚拟线程只要几 KB——内存开销差两个数量级。这也是为什么虚拟线程能支撑「thousands or millions」级别的并发，而平台线程到几千就顶不住了。\n但虚拟线程的核心价值不只是「轻」，而是它的调度机制——这才是它和 goroutine 一样优雅的地方。\n虚拟线程如何工作：阻塞就挂起 理解虚拟线程，先理解一个词：carrier（载体线程）。\nJVM 底层有一组平台线程专门用来运行虚拟线程（数量默认等于可用 CPU 核数），虚拟线程被调度器分配到这组 carrier 上执行。调度器是 JDK 内置的 work-stealing ForkJoinPool，FIFO 模式。\n%%{init: {'theme':'base'}}%% graph LR A[\"虚拟线程 1\"] --\u003e C[\"carrier 平台线程（调度器分配）\"] B[\"虚拟线程 2\"] --\u003e C D[\"虚拟线程 3\"] --\u003e C style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style D fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#fef9c3,stroke:#ca8a04,color:#713f12关键机制在阻塞发生时。看这个流程：\n%%{init: {'theme':'base'}}%% graph LR A[\"虚拟线程执行调用阻塞 I/O\"] --\u003e B[\"JVM 识别阻塞调用自动执行非阻塞 OS 调用\"] B --\u003e C[\"虚拟线程挂起释放 carrier\"] C --\u003e D[\"carrier 去运行其他虚拟线程\"] D --\u003e E[\"I/O 完成调度器恢复虚拟线程\"] E --\u003e A style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#d1fae5,stroke:#059669,color:#064e3b style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#d1fae5,stroke:#059669,color:#064e3b style E fill:#d1fae5,stroke:#059669,color:#064e3b当虚拟线程调用阻塞 I/O 时，JVM 运行时自动把阻塞调用替换成非阻塞 OS 调用，然后挂起虚拟线程、释放 carrier。carrier 立刻去运行别的虚拟线程，不空等。\n对比平台线程：一个平台线程阻塞在数据库查询或远程调用上，它只能空等，线程池里这个位置就被白白占用。200 个请求并发，200 个线程全在等 IO，第 201 个请求只能排队——这就是开篇那个场景。\nJEP 444 把这层意思说得很直白：线程数往往在 CPU 或网络连接等资源耗尽之前，就先成为限制因素（\u0026ldquo;the number of threads often becomes the limiting factor long before other resources, such as CPU or network connections, are exhausted\u0026rdquo;）。阻塞的线程不消耗 CPU，但占着线程名额——所以你会看到资源有余、并发却上不去的怪象。\n虚拟线程的思路是：我阻塞，但不占着茅坑。挂起时不消耗 carrier 资源，恢复后继续执行。这就是「thread-per-request」风格能跑通百万并发的根本原因——每个请求一个虚拟线程，阻塞时自动让位。\n调度开销：虚拟线程省下的第二个大头 上面讲的是「阻塞时不占线程」，虚拟线程还省下另一笔大账——调度开销。\n平台线程的调度由操作系统完成：切换线程时，内核要保存/恢复寄存器和栈状态，还可能导致 CPU 缓存失效，这是一笔昂贵的开销。线程越多，OS 花在调度上的时间比例越高，真正干活的 CPU 时间反而变少——这是平台线程并发数上不去的第二个原因，和内存占用并列。\n虚拟线程的调度完全不同：它由 JVM 的 ForkJoinPool 调度到 carrier 上运行，挂起和恢复都在用户态完成，只保存/恢复少量状态，不触发 OS 级上下文切换。JEP 444 原话是虚拟线程「cheap to create and almost infinitely plentiful」（创建便宜，数量近乎无限）。\n所以虚拟线程提高并发是双管齐下：\n成本维度 平台线程 虚拟线程 内存成本 1MB 栈/线程，几万线程就几十 GB 几 KB/线程，支持百万级 调度成本 OS 上下文切换，保存大量状态，缓存失效 JVM 用户态挂起/恢复，保存少量状态 内存和调度两个大头都降下来，并发数才能从「数千」跳到「数百万」。这也是为什么说虚拟线程是低成本提高并发的手段——不是堆硬件，而是把每个并发单元的代价做小。\n虚拟线程的边界：不是万能药 虚拟线程很强大，但有两个边界必须知道。\n边界一：CPU 密集型任务没优势 虚拟线程解决的瓶颈是「大量任务等待 I/O」。对 CPU 密集型任务——任务本身在计算，不阻塞——虚拟线程没有帮助。\n场景 虚拟线程效果 原因 IO 密集型（请求等待数据库/网络/文件） ✅ 巨大提升 阻塞期释放 carrier，并发数提升几个数量级 CPU 密集型（计算、加密、图像处理） ❌ 无优势 瓶颈是 CPU 核心数，不是线程数 CPU 密集场景的正确做法还是：合理分配任务到 CPU 核心，控制线程数。\n边界二：pinning——虚拟线程被「钉住」 正常情况下，虚拟线程阻塞时释放 carrier。但如果虚拟线程执行到 synchronized 块或 native 方法 时发生阻塞，它会被「钉」在 carrier 上——因为 JVM 无法在这些场景下安全地挂起和切换。\n%%{init: {'theme':'base'}}%% graph LR A[\"虚拟线程执行synchronized 块\"] --\u003e B[\"发生阻塞\"] B --\u003e C[\"被钉在 carrier 上carrier 无法释放\"] C --\u003e D[\"carrier 被占用其他虚拟线程排队\"] style A fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style B fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#fee2e2,stroke:#dc2626,color:#7f1d1d这不会导致应用错误（JEP 444 明确说 pinning 不使应用 incorrect），但会阻碍扩展性——被钉住的 carrier 不能服务其他虚拟线程。\n高频遇到 pinning 的场景：基于 JDBC 的数据库驱动（MySQL Connector/J、PostgreSQL JDBC 等）和 synchronized 方法。这也是为什么「虚拟线程 + 传统 JDBC 连接池」不是好组合——数据库连接是稀缺物理资源，连接池大小远小于虚拟线程并发数，大量虚拟线程会阻塞在等连接上，而 synchronized 又可能引发 pinning。\n回到开头 回顾整条问题链：\n平台线程 1:1 映射 OS → 重资源 → 并发受限于线程数 → 虚拟线程：用户态轻量级线程 → 阻塞就挂起，不占 carrier → IO 密集型受益巨大（百万级并发） → CPU 密集型无优势 → synchronized/native 场景可能 pinning 下次遇到 Java 服务并发上不去，先想清楚两件事：\n任务是 IO 密集还是 CPU 密集？ IO 密集才考虑虚拟线程，CPU 密集先优化计算本身 代码里有没有 synchronized / JDBC 阻塞？ 有的话，先解决 pinning 场景再上虚拟线程 虚拟线程不是银弹，但它确实把 Java 带进了「百万级并发」的赛道——前提是用对场景。\n本文基于 JEP 444（openjdk.org/jeps/444）官方规范与 Java 版本历史（Wikipedia）的梳理，平台线程默认栈 1MB、虚拟线程数千万级等数据均来自该来源。\n","date":"2026-08-13T00:00:00Z","image":"/images/java-understanding-virtual-threads-cover.png","permalink":"/post/java/understanding-virtual-threads/","title":"理解虚拟线程：从一次阻塞说起"},{"content":"你上过 AI 转型这条船吗？\n老板听说「AI 能提效 3-5 倍」，大手一挥：全员上 AI。工具买了、token 发了、KPI 也压下来了。半年后一复盘：员工确实更忙了，产出呢？说不清。公司该堵的流程还是堵，该乱的协作还是乱。\n问题出在哪？\nRAND 公司 2024 年发布了一份调研报告，采访了 65 位有五年以上经验的 AI 工程师，结论是：超过 80% 的 AI 项目会失败，失败率是普通 IT 项目的两倍。而失败的第一大根因，不是技术不行，而是——从一开始就没搞清要用 AI 解决什么问题。\n这篇文章把「没搞清问题」这件事拆开讲：四条一手血泪教训，一个统一框架，一条正确的转型路径。\n员工提效 ≠ 组织提效 先看最常见的误区：很多企业把 AI 转型理解成「给每个员工配一个 AI 助手」。\n听起来没毛病。但这里藏着一个偷换概念：员工提效了，组织未必提效。\n%%{init: {'theme':'base'}}%% graph LR A[\"企业 AI 转型两个方向\"] --\u003e B[\"赋能员工给每人配 AI 助手\"] A --\u003e C[\"重构流程用 AI 重做业务链路\"] B --\u003e D[\"员工更快了但流程没变、审批没变、协作没变\"] C --\u003e E[\"流程更快更顺组织整体提速\"] style A fill:#d1fae5,stroke:#059669,color:#064e3b style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style D fill:#fee2e2,stroke:#dc2626,color:#7f1d1d style E fill:#d1fae5,stroke:#059669,color:#064e3b为什么？因为组织的产出不是「员工产出之和」，而是「流程的产出」。一个员工用 AI 把写文档的时间从 2 小时压到 20 分钟，但如果文档还要走三层审批、跨三个部门等确认——节省的时间全部被流程吃掉了。\nRAND 报告里的原话是：很多项目失败是因为「模型被部署时优化的指标不对，或者根本不适合整体业务流程和上下文」。工具对了，流程没对，等于白干。\n这就是整篇文章的地基：AI 转型改的不是工具，是流程。\n一个框架：管理 = 管人 + 管事 为什么企业会在「赋能员工」上集体跑偏？因为 AI 转型的本质是管理问题，不是技术问题。而管理的基本功，AI 时代之前就要做好——管不好的人和事，加了 AI 只会放大问题，不会自动解决。\n维度 没 AI 时做不好 加了 AI 后 管人：培训、对齐目标、沟通机制、评估标准 团队各自为战，方向不一致 AI 工具碎片化，各自用各自的，更割裂 管事：流程设计、分工协作、质量标准、反馈闭环 流程堵、审批多、重复建设 AI 让流程跑得更快，堵得更严重 记住这个框架，下面的四条教训全都归到这两类里。\n四条血泪教训 以下来自一个真实团队的 AI 转型经历（信息已去敏）。每一条都是真金白银换来的。\n教训一：领导道听途说「AI 能提效 3-5 倍」，然后把员工压力翻到 3-5 倍 老板在行业活动上听到「AI 提效 3-5 倍」，回来直接变成 KPI：你们产出也要翻 3-5 倍。\n没做三件事：没先验证、没小范围试、没跑通再推广。员工还没来得及学会用 AI 把工作做好，工作量先上去了。\n结果：人更累了，质量更差了。AI 从工具变成了负担。\n归因：管事层面——流程没设计就加杠杆。这对应 RAND 报告第一根因：优化了错误的指标。KPI 定的是「产出翻倍」，但没人定义过「产出」到底是什么、流程哪一步该用 AI。\n教训二：只要求结果，不管过程 领导只看「产出翻了几倍」，不关心中间协作摩擦有没有变少。\nAI 工具塞进来，旧流程没变、旧审批没变、旧考核没变。每个人都在更快地产生半成品，然后半成品在旧的流程里排队、卡壳、来回返工。\n归因：管事层面——缺过程管理。RAND 报告里另一个根因是「模型不适合整体业务流程」：工具升级了，流程原地不动，等于在高速路上设红绿灯。\n教训三：没有统一培训，各自为战 团队每人用自己的方式用 AI：有人用 Superpower，有人用 OpenSpec，有人还在用 Cursor 的 Vibe Coding。\n从没坐下来沟通过：哪些场景用什么技术、为什么。\n归因：管人层面——缺对齐和沟通。工具没有标准，认知没有对齐，团队不是合力而是散力。RAND 报告里也提到「人与技术的交互问题」是失败的重要来源。\n教训四：只提供 token，不提供学习交流机会 → 重复建设 公司买了 AI 工具，说「你们自己用就行」，然后就没有然后了。\n没有交流机制，没有统一标准。AI 阅读引导文件（AGENTS.md / CLAUDE.md / .cursorrules）各搞各的，知识库建设各建各的。同样的基建工作，N 个人在重复做——浪费 token，也浪费人力。\n归因：管人层面——缺协作机制。基建没有沉淀成组织资产，每次都是从头再来。\n四条教训，一张图 %%{init: {'theme':'base'}}%% graph LR A[\"四条血泪教训\"] --\u003e B[\"管事流程没设计就加杠杆只要求结果不管过程\"] A --\u003e C[\"管人没培训各自为战没交流重复建设\"] B --\u003e D[\"AI 放大旧问题\"] C --\u003e D style A fill:#d1fae5,stroke:#059669,color:#064e3b style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style D fill:#fee2e2,stroke:#dc2626,color:#7f1d1d看这张图，你会发现四条教训没有一条是「AI 技术不行」。全是管理基本功没做好——AI 只是放大器：把好的放大成更好，把烂的放大成更烂。\n正确的转型顺序 先别急着上工具。正确顺序是：\n%%{init: {'theme':'base'}}%% graph LR A[\"第一步重构流程\"] --\u003e B[\"第二步重构岗位\"] B --\u003e C[\"第三步重构能力\"] A --\u003e D[\"先拆业务流程：哪一步耗时？哪一步是瓶颈？\"] B --\u003e E[\"再定岗位：哪些工作被 AI 替代？哪些岗位新增？\"] C --\u003e F[\"最后提能力：按岗位需要培训配对应工具\"] style A fill:#d1fae5,stroke:#059669,color:#064e3b style B fill:#d1fae5,stroke:#059669,color:#064e3b style C fill:#d1fae5,stroke:#059669,color:#064e3b style D fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style E fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style F fill:#dbeafe,stroke:#2563eb,color:#1e3a5f第一步：重构流程 这是最重要、也最反直觉的一步。大多数人想的是「先买工具、先让员工用起来」，但正确的顺序是反过来：先想清楚流程，再配工具。\n具体做法：\n画出关键业务流程 标记每一步：耗时多少？谁在做？是瓶颈吗？ 问一个问题：这一步有没有可能根本不需要人做？ 而不是「这一步怎么让人做得更快」 这一步的判断标准：AI 应该改变「要不要做」和「怎么做」，而不只是「做得快一点」。\n第二步：重构岗位 流程定了，岗位跟着变。\n有些岗位会消失（纯执行、纯转录的活），有些岗位会新增（提示词设计、AI 流程运维、结果质检）。这不是裁员信号，是重新分配人的注意力——把人的精力从「重复劳动」挪到「判断和决策」上。\n第三步：重构能力 最后才是培训。这时候培训才有意义，因为你已经知道：\n哪个岗位需要什么能力 用哪个工具 按什么标准验收 这时候统一培训、统一标准、统一知识库——教训三、四里的坑就绕开了。\n中小团队的行动建议 大型企业有专门的转型部门，中小团队只能靠自己。给中小团队一张可以直接执行的清单：\n顺序 动作 对应教训 第 1 周 画出 1 条核心业务流程，标出最痛的一步 教训一 第 2 周 只在这一个点上试点 AI，小范围验证 教训一 第 3 周 定义清楚「这个点做好了」的标准（不是「快了多少」，是「质量达标且稳定」） 教训二 第 4 周 团队坐下来：统一工具、统一引导文件、建共享知识库 教训三、四 持续 每周一次 30 分钟复盘：哪里卡住了、谁发现了新用法 教训三、四 记住开头那个问题：你上过 AI 转型这条船吗？\n如果你正准备上船，先别急着买工具。先画流程，再想岗位，最后才是配工具。AI 不是让低效跑得更快的引擎，是让高效跑得更远的引擎——前提是，你的流程先得高效。\n本文观点来自老任团队 AI 转型实践梳理，结合 RAND《The Root Causes of Failure for Artificial Intelligence Projects and How They Can Succeed》(RR-A2680-1, 2024-08-13) 与老章《很多企业走 AI 转型，一开始就错了》的交叉验证。\n","date":"2026-08-13T00:00:00Z","image":"/images/ai-transformation-common-mistakes-cover.png","permalink":"/post/ai-transformation-common-mistakes/","title":"企业 AI 转型的常见错误：从四条血泪教训说起"},{"content":"一个奥运冠军，民族英雄。\n两亿身家。\n七年之后——卖房、租房、夫妻冷战、直播带货。\n你可能会说：这家人不会理财。但如果问题仅仅是\u0026quot;不会理财\u0026quot;，那答案很简单——请个理财顾问就好了。为什么他们没有？\n因为有些认知缺陷，花多少钱都请不到人来填。\n第一层：为什么这条新闻能火三周 我们先回答一个最表面的问题：这条新闻凭什么能火上两三周？\n拆开来看，要素太完整了：\n名人——奥运拳击冠军邹市明，中国拳击界唯一的大满贯 金钱——两亿，一个普通人几辈子赚不到的数字 婚姻——夫妻冷战争吵，多次闹离婚，三个孩子 背叛疑云——网络传言冉莹颖转移财产 落差感——曾经的民族英雄卖房租房，靠直播带货维生 每一个要素都在戳大众的心理敏感点。\n而且，这条新闻没有认知门槛。你不需要懂 AI、不需要懂上市公司、不需要有任何专业知识——任何人都能评两句。\n老梁在视频里说得直白：\u0026ldquo;看别人倒霉，有时候比升官发财还高兴。\u0026rdquo;\n仇富心理在作祟（\u0026ldquo;让你有钱，赔了吧\u0026rdquo;）、道德制高点（\u0026ldquo;败家娘们\u0026quot;\u0026ldquo;没常识\u0026rdquo;）、代入感（\u0026ldquo;名人跟我一样一地鸡毛\u0026rdquo;）——三重快感叠加，热度自然下不来。\n%%{init: {'theme':'dark'}}%% flowchart LR A[\"新闻传播要素名人·金钱·婚姻·落差\"] --\u003e B[\"大众心理触发\"] B --\u003e C[\"仇富心理看别人赔了心里爽\"] B --\u003e D[\"道德制高点谁都能评两句\"] B --\u003e E[\"代入感名人一样一地鸡毛\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12 style E fill:#fef9c3,stroke:#ca8a04,color:#713f12但你要知道——综艺节目不是纪录片。任何一档综艺，本质都是出镜者与制作方的合谋。亏两亿是真，但上综艺暴露家庭矛盾，本身就是流量剧本。冉莹颖已经在带货了，她需要流量。反常必有妖。这个问题我们最后还会回来。\n第二层：两亿，一拳一拳打出来的 在讨论\u0026quot;怎么没的\u0026quot;之前，先搞清楚\u0026quot;怎么来的\u0026rdquo;。\n邹市明的成就，放在中国体育史上，分量极重：\n里程碑 意义 2008 北京奥运 48kg 金牌 中国拳击首枚奥运金牌 2012 伦敦奥运 48kg 金牌 中国拳击首枚奥运卫冕 世锦赛冠军 多项\u0026quot;第一次\u0026quot;创造者 WBO 蝇量级金腰带 转职业后世界冠军 历史地位 中国拳击界唯一大满贯 但这成就的背后，是拳击运动的残酷本质。\n\u0026ldquo;未学打人，先学挨打。\u0026rdquo;\n看一场拳击比赛，获胜者脸上往往也是眉骨裂了、眼睛肿了、满脸淤青。老梁展示了惊人的数据：拳击手平均寿命只有 67 岁，远低于普通人群。邹市明训练多年，至今已有一只眼几近失明，手抖不止。\n更要命的是，中国拳击 1959 年到 1983 年被禁了 24 年——周恩来认为这项运动\u0026quot;残忍血腥\u0026quot;。解禁后被长期视为边缘运动。直到今天，中国注册职业拳手只有 490 人，在 14 亿人口中几乎可以忽略不计。\n%%{init: {'theme':'dark'}}%% flowchart LR A[\"16岁进入拳击队\"] --\u003e B[\"业余巅峰2008·2012奥运金牌\"] B --\u003e C[\"职业转型WBO金腰带\"] C --\u003e D[\"时代红利金牌≈民族英雄\"] D --\u003e E[\"两亿身家拳拳血汗钱\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style C fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style D fill:#fef9c3,stroke:#ca8a04,color:#713f12 style E fill:#ecfdf5,stroke:#059669,color:#064e3b邹市明能挣到两亿，一方面确实是靠一拳一拳打出来的血汗，另一方面也有时代红利——2008 年北京奥运是举国体制的巅峰，民族情绪空前高涨。在那个时代，\u0026ldquo;金牌得主≈民族英雄\u0026rdquo;，商业价值自然水涨船高。如果晚生十年，同样的成就可能换不回十分之一的商业回报。\n两亿是真的，但其中很大一部分是时代的馈赠——不是他们自己的能力。\n第三层：两亿怎么没的 钱来得不容易，花出去倒是快得很。\n最大的一个坑：上海黄浦江边 18000 平方米的健身中心。\n项目 数据 面积 18000 平方米 年租金 约 5000 万 设备投入 吊灯 300 万、跑步机 20 万/台 经营结果 仅一个月盈利，每月亏损上百万 总烧钱 约 1.2 亿 一上来就是超重型资产，一出手就是一个多亿。\n这里面有两个致命误判：\n%%{init: {'theme':'dark'}}%% flowchart LR subgraph 误判1[\"误判①\"] A1[\"奥运冠军IP会一直值钱\"] A2[\"实际：2008年后逐年级递减\"] end 误判1 --\u003e 原因1[\"原因：举国体制降温年轻人对金牌情感淡化\"] subgraph 误判2[\"误判②\"] B1[\"拳击在中国有大众市场\"] B2[\"实际：注册拳手14亿人中仅490人\"] end 误判2 --\u003e 原因2[\"原因：曾被禁24年家长认为打拳=暴力\"] 原因1 --\u003e 结论[\"健身中心月租400万+无客源\"] 原因2 --\u003e 结论 style 误判1 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style A1 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style A2 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style 误判2 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style B1 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style B2 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style 原因1 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style 原因2 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style 结论 fill:#dc2626,stroke:#991b1b,color:#fff误判一：奥运冠军 IP 会一直值钱。\n2008 年后，奥运冠军的商业价值逐年递减。举国体制在降温，年轻一代对奥运金牌的情感绑定在减弱。到了 2017-2018 年，再靠\u0026quot;奥运冠军\u0026quot;四个字撑起一个月租 400 多万的商业体，已经不现实了。\n误判二：拳击在中国有大众市场。\n14 亿人口，注册职业拳手 490 人。被禁了 24 年、到今天多数家长仍觉得\u0026quot;打拳=暴力\u0026quot;的运动——怎么可能撑起 18000 平米的高端健身场馆？\n亏完健身中心之后，两口子\u0026quot;狗急跳墙\u0026quot;——火锅店、奶茶店、虚拟货币，什么都试，什么都赔。\n%%{init: {'theme':'dark'}}%% flowchart LR A[\"两亿身家\"] --\u003e B[\"健身中心烧掉1.2亿\"] B --\u003e C[\"连锁反应\"] C --\u003e D[\"火锅店\"] C --\u003e E[\"奶茶店\"] C --\u003e F[\"虚拟货币\"] D --\u003e G[\"归零\"] E --\u003e G F --\u003e G style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style E fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style F fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style G fill:#dc2626,stroke:#991b1b,color:#fff这里有一个老梁反复追问的问题：两亿在手，存银行吃利息不香吗？为什么非要冒归零的风险？\n答案不是\u0026quot;他们不懂理财\u0026quot;那么轻巧。问题的根源，比理财深得多。\n第四层：为什么管不住——双方共同的认知缺陷 两亿不是一个人花完的。邹市明和冉莹颖各有各的认知问题，且方向不同，但结果一致。\n冉莹颖：伪精英的陷阱 冉莹颖上了北大光华 MBA（准确说是 EMBA）。老梁直言——\u0026ldquo;不上还好，上了坏了。\u0026rdquo;\n为什么？\nEMBA 班里的同学都是成功人士，或者在成功路上的人。她听到的全是幸存者偏差——99 个失败案例不会出现在课堂里，上台分享的都是那 1% 的成功者。她听完之后产生了一个致命的错觉：\u0026ldquo;我跟他们是同类人。我也有两亿，我也是企业家。\u0026rdquo;\n但别人的成功有时代背景、有运气成分、有个人禀赋。她全不具备。\n她的身份焦虑进一步放大了这个错误。她曾是央视证券频道外包节目的出镜主持人——连央视编制都不是，但她从不澄清。北大 EMBA 也是花钱就能上的\u0026quot;学历\u0026quot;。中考考过状元，但高考考得一般——所以永远只提\u0026quot;中考状元\u0026quot;。\n她要证明一件事：\u0026ldquo;我不是靠老公的女人，我是女强人。\u0026rdquo;\n所以必须折腾。哪怕方向错了，也比不折腾强——因为不折腾就等于承认\u0026quot;我确实没本事\u0026quot;。\n邹市明：运动员思维的局限 邹市明这边的问题更隐蔽。\n中国体育的\u0026quot;三级输送体制\u0026quot;（业余体校→省体校→国家队）决定了运动员从小过集体生活，文化课极少，接触的人不超过 20 个——教练、队友、领导。长期下来，对人际关系的判断力极弱，对社会运作的认知极度有限。\n更关键的是，这个体制有极强的洗脑效应：每天接受的是集体主义、为国争光、国家荣誉高于一切。作为奥运冠军、旗帜性人物，邹市明的\u0026quot;被洗脑程度\u0026quot;尤其深。\n这导致什么？容易被热血故事打动。冉莹颖说几句\u0026quot;我们能做到贝克汉姆那样\u0026quot;，他就信了。\n老梁用三句顺口溜概括了钱感错位的本质：\n类型 口诀 特征 种地的钱 \u0026ldquo;种地钱，万万点\u0026rdquo; 每一分都在土里刨，极度珍惜 做生意的钱 \u0026ldquo;马上钱，花十年\u0026rdquo; 流动状态，会花也会珍惜 艺人的钱 \u0026ldquo;艺人钱，当天完\u0026rdquo; 空手来财，敢当天花光 邹市明是\u0026quot;种地钱\u0026quot;心态——一拳一拳打出来的钱，每一分都是血汗。但冉莹颖是\u0026quot;艺人钱\u0026quot;心态——觉得老公是 IP 明星，随时能挣回来。\n两人对钱的来路认知不一致，导致花法也完全不一致。\n%%{init: {'theme':'dark'}}%% flowchart LR subgraph 邹[\"邹市明\"] A1[\"种地的钱一拳一拳打出来极度珍惜每一分\"] end subgraph 冉[\"冉莹颖\"] A2[\"艺人的钱觉得随时能挣回来敢花敢投敢冒险\"] end 邹 --\u003e 冲突{\"对钱的认知不一致\"} 冉 --\u003e 冲突 冲突 --\u003e 结果[\"花法冲突投资决策失控两亿归零\"] style 邹 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A1 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 冉 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style A2 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style 冲突 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style 结果 fill:#dc2626,stroke:#991b1b,color:#fff对比：贝克汉姆的认知碾压 冉莹颖说过一句话：\u0026ldquo;我想把邹市明打造成中国的小贝。\u0026rdquo;\n问题在于，她只学了\u0026quot;小贝\u0026quot;的名号，没学\u0026quot;小贝\u0026quot;的商业模式。\n贝克汉姆的经营逻辑是绝对轻资产：\n从不碰实体资产——不开店、不办场馆 把 IP 卖给专业公司运营，自己当股东 只做自己最强的事——维护个人品牌和形象 收入来源：转会分成、品牌授权、商业代言 而冉莹颖的路径完全相反：\n一上来就是 18000 平米的重资产 所有事自己操盘——从健身中心到火锅店到奶茶店 每一个领域都不是她的专业 美其名曰\u0026quot;运筹帷幄\u0026quot;，实际上连一线业务都不熟悉 老梁有一个精准的判断：中小公司老板初始阶段不深入一线，一定干不好。 老板跟老板谈出来的单子，和业务员跟业务员谈出来的单子，天差地别。\n%%{init: {'theme':'dark'}}%% flowchart LR subgraph 贝[\"贝克汉姆模式\"] B1[\"只做最强项IP维护与品牌授权\"] B2[\"轻资产运营不碰实体店/场馆\"] B3[\"收入来源转会分成·代言·授权\"] end 贝 --\u003e 核1{\"核心：发挥禀赋\"} 核1 --\u003e 结1[\"稳：吃几十年\"] subgraph 冉[\"冉莹颖模式\"] R1[\"什么都要管健身·火锅·奶茶·虚拟币\"] R2[\"重资产开局18000㎡起步\"] R3[\"亲自操盘CEO运筹帷幄\"] end 冉 --\u003e 核2{\"核心：高估自己\"} 核2 --\u003e 结2[\"危：七年归零\"] style 贝 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B1 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B2 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B3 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 核1 fill:#d1fae5,stroke:#059669,color:#064e3b style 结1 fill:#d1fae5,stroke:#059669,color:#064e3b style 冉 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style R1 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style R2 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style R3 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style 核2 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style 结2 fill:#dc2626,stroke:#991b1b,color:#fff第五层：综艺是真惨还是剧本 这可能是整个事件中最值得玩味的一层。\n回到第一层的问题：为什么要在综艺上暴露家丑？\n逻辑链条是这样的：\n确实亏了两亿，需要收入来源 消费降级时代，明星变现路径只剩带货 带货需要流量，流量需要话题 上综艺制造矛盾→获得关注→引流带货 %%{init: {'theme':'dark'}}%% flowchart LR A[\"亏了两亿需要收入来源\"] --\u003e B[\"消费降级时代变现路径只剩带货\"] B --\u003e C[\"带货需要流量\"] C --\u003e D[\"流量需要话题\"] D --\u003e E[\"上综艺暴露家丑获取关注\"] E --\u003e F[\"引流带货赚认知的钱\"] F -.-\u003e A style A fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style B fill:#fef9c3,stroke:#ca8a04,color:#713f12 style C fill:#fef9c3,stroke:#ca8a04,color:#713f12 style D fill:#fef9c3,stroke:#ca8a04,color:#713f12 style E fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style F fill:#d1fae5,stroke:#059669,color:#064e3b\u0026ldquo;局中局\u0026rdquo;——他们已经通过暴露自己的认知缺陷，反过来赚认知的钱。\n网上有人传言冉莹颖转移财产。老梁逐一反驳：\n就算亲戚捞了几千万，跟亏两亿比九牛一毛 真要转移财产，需要懂股权架构、税务规划、灰色法律——她三条一条都不具备 从健身中心到火锅店，经营时间跨度长达 7 年——\u0026ldquo;转移财产是快进快出，哪有搞七年的\u0026rdquo; 所有财务记录公开透明，审计部门一查就有 结论很简单：不是坏，是菜。\n第六层：核心认知——守不住认知以外的钱 到了这里，我们可以回答那个最根本的问题了。\n有人说\u0026quot;人赚不到认知以外的钱\u0026quot;。老梁纠正了这句话：人能挣到认知以外的钱（靠运气、时代红利、禀赋），但很难守住。\n%%{init: {'theme':'dark'}}%% flowchart LR subgraph 挣[\"能挣到认知以外的钱\"] A1[\"拆迁暴富\"] A2[\"踩中风口煤老板·房地产\"] A3[\"吃到时代红利奥运金牌商业价值\"] end 挣 --\u003e 过渡{\"钱在手里\"} 过渡 --\u003e 守[\"但守不住\"] 守 --\u003e 结果[\"瞎投资→赔光返贫\"] 过渡 -.-\u003e 守错[\"认知不到的事不去碰\"] 守错 --\u003e 结果2[\"钱保住了\"] style 挣 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A1 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A2 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A3 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 过渡 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style 守 fill:#fca5a5,stroke:#dc2626,color:#7f1d1d style 结果 fill:#dc2626,stroke:#991b1b,color:#fff style 守错 fill:#d1fae5,stroke:#059669,color:#064e3b style 结果2 fill:#d1fae5,stroke:#059669,color:#064e3b拆迁暴富、踩中风口、吃到时代红利——钱可以来得很容易。但这笔钱如果超出了你的认知层级，它就不会在你手里待太久。你总会有办法把它\u0026quot;还回去\u0026quot;。\n邹市明挣两亿靠的是拳击，这完全在他的认知以内。但冉莹颖用两亿去做生意——这完全在他们的认知以外。\n最危险的不是穷，是返贫。\n通缩时代：不折腾比瞎折腾强 老梁最后给出了一个时代背景判断，把个案上升到策略层面。\n当下中国的企业格局大致是：\n企业类型 占比 特征 国家战略赛道 \u0026lt;10% AI、新能源、芯片——政策资金全力倾斜 配套企业 25-30% 给前者做配套，需考虑国有资本进入后的壁垒 普通民企 60%+ 不在高速列车上，被时代推着走 大部分人和企业，属于那 60%——不在国家战略赛道上。在这个群体里，通缩下行的环境下，最理性的选择是什么？\n老梁给出的建议很实在：\n判断自己是否在国家战略赛道上 判断自己能否在国有资本进入时建立壁垒 如果没有持续现金流，保持五年以上的安全现金流 如果上述都不具备——躺平、体面退出、压缩规模 %%{init: {'theme':'dark'}}%% flowchart LR A[\"通缩时代生存四问\"] --\u003e B[\"我在国家战略赛道？\"] B --\u003e|\"是\"| C[\"全力投入\"] B --\u003e|\"否\"| D[\"我有持续现金流？\"] D --\u003e|\"有\"| E[\"保五年安全现金流\"] D --\u003e|\"无\"| F[\"战略性收缩不折腾\"] style A fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B fill:#fef9c3,stroke:#ca8a04,color:#713f12 style C fill:#d1fae5,stroke:#059669,color:#064e3b style D fill:#fef9c3,stroke:#ca8a04,color:#713f12 style E fill:#d1fae5,stroke:#059669,color:#064e3b style F fill:#fca5a5,stroke:#dc2626,color:#7f1d1d老梁把人分成了四种：\n类型 特征 评价 懒惰的聪明人 会选人、会用人，不动手 核心决策者 勤奋的聪明人 在正确道路上高效执行 最好的执行者 懒惰的笨蛋 推都不动 干些不动脑的活 勤奋的笨蛋 在错误道路上狂奔 破坏力最大 冉莹颖就是典型的\u0026quot;勤奋的笨蛋\u0026quot;——努力、拼命，但方向全是错的。在错误路线上，越努力，破坏力越大。\n不折腾可能意味着没有损失那么多，至少能把生活根本保住。\n你能挣到认知以外的钱，但很难守住。\n这句话，邹市明夫妇用两个亿交了学费。但学费已经有人付过了，你不需要再付一次。\n本文观点来自老梁视频的梳理与分析。\n","date":"2026-07-27T00:00:00Z","image":"/images/neng-zheng-dan-shou-bu-zhu-cover.png","permalink":"/post/neng-zheng-dan-shou-bu-zhu/","title":"能挣到认知以外的钱，但守不住"},{"content":"2026 年 7 月 7 日，高善文走了。55 岁，T 细胞淋巴瘤。\n这个名字对不关注宏观经济的人可能陌生。他是安信证券的首席经济学家，业内公认的中国宏观分析第一人。他以数据驱动、逻辑严密著称，多次准确预判经济拐点。但在生命的最后几年，他因为一场演讲被禁言，社交账号被封，淡出公众视野。\n那场演讲是 2018 年的，山西证券 30 周年活动上。\n演讲里他搞了一个多小时的经济分析，但流传最广、争议最大、也最让人无法回避的，是其中一小段——关于中国人理解世界的方式。他说：\n中国人理解世界的方式，不是观察和逻辑推导，而是阴谋论和类比。翻开二十四史，每一页都写满了阴谋。讲复杂道理听不懂，打个比方他马上就明白了。\n这话很刺耳。但如果你愿意放下防御心，仔细想一想，会发现它指向了一个比任何经济问题都更根本的问题——\n你用什么方式理解这个世界？\n高善文到底说了什么 先还原他说这话的上下文，免得断章取义。\n高善文不是在做文化批评，他是在解释一个困惑：为什么中国 180 年的现代化道路如此艰难。他的诊断是：不是不够努力，不是不够聪明，而是在重大历史关头的认知方式出了问题。\n他对比了两种认知工具：\n西方思维 中国思维 核心工具 客观观察 + 逻辑推导 阴谋论 + 类比 典型表现 测量、实验、因果推理 「背后一定有人搞鬼」「打个比方说」 历史根源 古希腊理性传统 + 启蒙运动 二十四史权谋叙事 + 象形文字 他不是说中国没有逻辑——中国有文言文、有考据学、有辩证逻辑。他说的是在重大选择面前，我们下意识依赖的工具。\n证据？他举了 180 年的历史：\n1840 年鸦片战争，面对工业文明的冲击，选择「师夷长技以制夷」——用类比理解西方：不过是船坚炮利，我们的道德文化更高。1895 年甲午战败，洋务运动的赌注全输。1949 年后的三十年，再次用阶级斗争的叙事解释经济问题。直到 1978 年改革开放，邓小平「蒙对」了——没有用任何宏大叙事，只是观察、尝试、调整。\n180 年里，多数时候站在了错误的方向。\n这个判断有多大的覆盖面不好说，但有一个现象他指出来了：当中国人和美国人坐在一起谈判时，「永远是两条平行线」。中国人说「太平洋足够大，可以容纳两个超级大国」——这是一个类比。美国人听完一脸困惑：你到底想表达什么？\n象形文字塑造的思维 高善文没有深入讨论的一个维度，是语言本身。\n有一个经典的假说叫萨丕尔-沃尔夫假说（Sapir-Whorf hypothesis），核心观点就一句话：语言影响思维。你用什么语言表达世界，会影响你怎么理解世界。\n汉字是表意文字（象形），拼音文字是表音文字。这个差异远不止写起来不一样——它对应的是完全不同的认知路径。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR subgraph 汉字[\"🀄 汉字：表意文字\"] A1[\"看到字形\"] --\u003e A2[\"联想意象\"] A2 --\u003e A3[\"理解含义\"] end subgraph 拼音[\"🔤 拼音文字\"] B1[\"看到字母\"] --\u003e B2[\"拼读音节\"] B2 --\u003e B3[\"理解含义\"] end style 汉字 fill:#fef3c7,stroke:#d97706,color:#78350f style 拼音 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f读汉字时，大脑右半球（空间、整体、意象处理）会更多地参与进来。读拼音文字时，大脑左半球（语言、序列、逻辑处理）是主导。这不是猜测——脑功能成像研究已经反复验证了这个差异。\n这不是说一种比另一种高级。任何一种语言都能表达复杂的思想。问题是：当你习惯了通过意象联想来理解世界，你可能会不自觉地回避逻辑链条的展开。\n打个比方（你看，这就是类比思维）——如果你习惯了看一幅画来理解一个故事，你就不太容易养成逐句推理的习惯。不是不能推，是不习惯。而不习惯的后果是，当有人抛出一个精心构造的比喻时，你很难在第一时间发现那个比喻里的逻辑漏洞。\n类比思维的双刃剑 到这里需要做一个重要的纠偏：不是说类比不好，也不是说中国人不会逻辑。\n跨文化心理学在这方面的研究更公允。Nisbett 的经典著作 The Geography of Thought（《思维的地域性》）系统比较了东亚人和西方人的认知风格差异：\n维度 东亚认知风格 西方认知风格 注意力 整体性——关注背景和关系 分析性——关注对象和类别 归因 情境归因——行为由环境决定 特质归因——行为由内在特质决定 推理 辩证——接受矛盾共存 逻辑——排中律，非此即彼 解释方式 关联性思维——「事物是相互联系的」 因果性思维——「A 导致 B」 2019 年发表在 Taylor \u0026amp; Francis 上的一篇综述进一步深化了这个框架：研究者区分了「朴素辩证思维」（naïve dialectical thinking）和「线性思维」（linear thinking），认为两者的差异源于——个体主义 vs 集体主义的社会结构、分析性 vs 整体性的认知习惯、以及不同的哲学传统。\n所以高善文的判断在学术框架里是有支撑的。中国人确实更倾向于用类比、关联、辩证的方式来理解世界。这不是缺陷，是特征。但当这个特征独占了你的认知工具箱，问题就来了。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 认知工具[\"认知工具箱\"] --\u003e 类比[\"类比思维整体·关联·辩证\"] 认知工具 --\u003e 逻辑[\"逻辑思维分析·因果·验证\"] 类比 --\u003e 优势[\"优势：快速理解复杂概念\"] 类比 --\u003e 盲区[\"盲区：容易忽略逻辑漏洞\"] 逻辑 --\u003e 优势2[\"优势：可验证可推翻\"] 逻辑 --\u003e 盲区2[\"盲区：抽象难懂缺乏画面感\"] style 认知工具 fill:#fef3c7,stroke:#d97706,color:#78350f style 类比 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 逻辑 fill:#d1fae5,stroke:#059669,color:#064e3b类比是强大的认知工具。物理学家费曼用「台球碰撞」解释量子力学，爱因斯坦用「坐光速行驶的火车」解释相对论——这些都是类比。但费曼和爱因斯坦在做完类比后，会用数学公式和实验数据去验证那个类比是否站得住脚。\n高善文说的「阴谋论+类比」的真正问题不是用了类比，而是只用类比。当你说「经济下行就像一个人生病了」，然后用这个类比推导出「所以应该这样治」——你在做的是把两个可能毫不相干的事物用相似性连接起来，而不是在建立因果关系。\n一个典型的例子：有人说「中国经济就像 90 年代的日本，所以会失去三十年」。这是一个类比。但中国和日本的人口结构、城镇化率、国际环境、产业升级路径完全不同。用类比推导出的结论，可能离真相很远。但你很难用「这是个类比，所以不一定成立」去说服一个人——因为在他的认知框架里，类比就是论证。\n认知上的窗口期 高善文在演讲的最后说了这样一段话：\n对 30 岁以下的年轻人来讲，如果这次没搞对，大家就回去吃屎吧。我们这个年纪的人已经无所谓了，但是对年轻人来讲，我们确实站在一个非常重要的选择关头。\n这话说得非常重。但联系上下文看，他说的不仅是经济政策的对错，更是认知方式上的选择。\n你用什么方式理解世界，决定了你在关键时刻能做出什么样的判断。\n用类比理解的半懂不懂，以为自己懂了；用逻辑推导可能到不了 100% 的确定性，但你知道自己推导到了哪一步、哪里还不确定。后一种方式不保证结论正确，但它保证你能发现自己哪里错了——而这恰恰是前一种方式做不到的。\n意识到自己用的是哪种思维方式，本身就是一种认知升级。不是要你放弃类比——它是人类认知的基础能力。而是至少做到：当你用了一个类比的时候，知道自己在用类比，而不是把它当成了论证。\n高善文走了。一个敢用数据说话、敢于指出房间里那头大象的经济学家，在 55 岁离开了。但他提出的那个问题还在——你用什么方式理解这个世界？\n如果你觉得这话有道理，那不妨从一件小事开始：下次读到一篇用「打个比方说」开头的分析文章时，停一下，问自己一个问题——这个比方真的成立吗？\n参考资料\n高善文，山西证券30周年演讲，2018-07-28。高博因该演讲被禁言，2026年7月7日病逝。 Sapir, E. (1929). The Status of Linguistics as a Science. Linguistic relativity 经典论述。 Nisbett, R. E. (2003). The Geography of Thought: How Asians and Westerners Think Differently…and Why. Free Press. Explanations for cultural differences in thinking: Easterners\u0026rsquo; dialectical thinking and Westerners\u0026rsquo; linear thinking. Taylor \u0026amp; Francis, 2019. Brain activation in the processing of Chinese characters and words: A functional MRI study. PMC, 2021. Guan, T. (2024). Conspiracy Theories in Contemporary China: A Review. ResearchGate. 经济观察网，纪念高善文：那个敢说真话、爱自嘲的知名经济学家走了，2026-07-07. RFI，《华尔街日报》回顾高善文生命最后一年，2026-07-15. ","date":"2026-07-17T00:00:00Z","image":"/images/gaoshanwen-thinking-cover.png","permalink":"/post/gaoshanwen-thinking/","title":"高善文说的「阴谋论与类比」：中国人的思维方式到底怎么了"},{"content":"你有没有这样一种感觉——\n2023 年用上 GPT-4 的时候，那种震撼是真实的：它居然能写代码、能推理、能理解上下文。每发布一个新版本，你会迫不及待去试。但现在呢？GPT-5.6 发布了、Claude Fable 5 来了、Kimi K3 也出来了——你的第一反应是不是变成了：「哦，又强了一点。」\n不是它们不够强。GPT-5.6 Sol 在复杂推理上确实甩开了上一代一大截。问题在于：你 90% 的日常任务，两三年前的模型就已经够用了。\n这不是观点，是正在发生的现实。这篇不聊模型有多强，聊一个更实际的问题——为什么大部分 AI 项目依然在失败，以及真正该花精力在什么地方。\n2026 年的模型版图 先看看我们现在有什么。\n把今天的主流模型放在一张图里，按能力和价格两个维度看：\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 经济层[\"💰 经济层$0.10-0.50/1M 输入\"] --\u003e|\"80% 日常任务\"| 日常[\"分类/提取/摘要客服/简单推理\"] 主力层[\"🔋 主力层$2-3/1M 输入\"] --\u003e|\"15% 复杂任务\"| 复杂[\"多步推理长文档分析\"] 旗舰层[\"🚀 旗舰层$5-10/1M 输入\"] --\u003e|\"5% 特殊场景\"| 特殊[\"高难度代码研究级分析\"] style 经济层 fill:#d1fae5,stroke:#059669,color:#064e3b style 主力层 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 旗舰层 fill:#fef3c7,stroke:#d97706,color:#78350f style 日常 fill:#d1fae5,stroke:#059669,color:#064e3b style 复杂 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 特殊 fill:#fef3c7,stroke:#d97706,color:#78350f数据来源：OpenRouter API（2026-07 实时定价），涵盖 344 个模型。OpenAI GPT-5.6 于 2026 年 7 月 9 日发布，Claude Fable 5 于 6 月 9 日发布。DeepSeek V4 Pro 于 4 月开源，1.6T 参数/49B 活跃参数，1M 上下文。Kimi K3 据称可媲美 OpenAI 和 Anthropic 的旗舰模型。\n这张图的信息量比看起来大。注意定价跨度——旗舰层和主力层之间是 5-10 倍的价格差，和 DeepSeek V4 Flash 比更是 100 倍的差距。更深层的信号是：经济层的模型能力线已经远远到了日常任务「够用」的基准线之上。 日常任务不需要旗舰模型的原因不是「没钱」，是「没必要」。\n80/15/5 的分层现实 那实际在用的时候是怎么选的？\n以我托管的 hermes-home 多 Agent 系统为例——它跑着 7 个 Agent，每天处理调研、写作、决策分析、知识管理等任务。实际用量比例：\n层级 模型 用量 场景 经济层 DeepSeek V4 Pro（$0.44） ~80% 日常推理、内容生成、知识检索、决策辅助 主力层 GPT-5.6 Luna（$1.00） ~15% 需要更强推理能力的复杂任务 旗舰层 GPT-5.6 Sol / Claude Fable 5 ~5% 高难度代码生成、架构设计、研究级分析 这不是预算问题。换成 DeepSeek V4 Pro 每百万 token 只需 $0.44，全跑旗舰模型（Claude Fable 5 $10/1M）也就多花二十几倍。真正的原因是：对于 80% 的任务，换旗舰模型的提升用户感知不到。\n写一篇博客、整理一份调研、做一个决策分析——这些任务 DeepSeek V4 Pro 完成得很好。只有当你需要处理极其复杂的推理链条、或者生成需要深度理解上下文的高难度代码时，旗舰模型的优势才会显现。而且即便是这些时候，差距也在快速缩小：DeepSeek V4 Pro 的 Benchmark 已经追平了两年前的旗舰模型。\n现在，旗舰模型针对的是「不可能轻松做到的业务」，是「需要博士级的研究分析」，是「5% 的场景」。大部分公司的绝大部分业务和大部分个人的日常任务，处在那个 80% 的范围内。\nAI 项目真正的死因 如果模型已经够用了，那为什么那么多 AI 项目还是失败了？\nRAND 公司对 65 个企业 AI 项目的元分析给出了一个发人深省的数字：80% 的企业 AI 项目以失败告终。 Gartner 的追踪数据给出了类似的结论，MIT 的研究甚至将这个比例推高到了 95%。这些项目不是用 DeepSeek V4 Pro 做的——很多花了大价钱上了最好的模型。\n导致项目失败的原因，排在最前面的从来不是「模型不够聪明」。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 模型[\"📈 模型能力飞速提升\"] --\u003e|\"但……\"| 失败[\"⚠️ 80-95% 项目失败\"] 失败 --\u003e 根因[\"为什么失败？\"] 根因 --\u003e 需求[\"❌ 需求没挖对解决了错误的问题\"] 根因 --\u003e 成本[\"❌ 成本跑飞了不分层 \u003e 预算超支\"] 根因 --\u003e 工程[\"❌ 工程不靠谱缺乏可观测性和错误处理\"] style 模型 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 失败 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style 根因 fill:#fef3c7,stroke:#d97706,color:#78350f style 需求 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style 成本 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style 工程 fill:#fecaca,stroke:#dc2626,color:#7f1d1d数据来源：RAND 企业 AI 元分析（65 个项目，80% 失败率）、MIT 研究（95% 企业 AI 项目未交付价值）。\n我跑了几个月多 Agent 系统，最深的感受就是：模型从来不是瓶颈。 出错最多的地方，永远是需求没对齐、成本估算偏差、以及系统工程的细节。\n真正卡脖子的三件事 如果瓶颈不是模型，那是什么？基于实战观察，我认为有三件事比模型选型重要得多。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 旧认知[\"过去以为瓶颈是模型能力\"] --\u003e 新认知[\"实际瓶颈（三件事）\"] 新认知 --\u003e 需求挖掘[\"🔍 需求挖掘解决对的问题\"] 新认知 --\u003e 成本控制[\"💰 成本控制分层选型不跑偏\"] 新认知 --\u003e Agent工程[\"⚙️ Agent 工程可靠系统需要 Engineering\"] style 旧认知 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style 新认知 fill:#fef3c7,stroke:#d97706,color:#78350f style 需求挖掘 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 成本控制 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style Agent工程 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f需求挖掘：你确定你在解决对的问题？ 大部分 AI 项目死得冤枉。团队花了几周时间搭建一个完美的 RAG 流水线，上线后发现用户根本不需要这个功能。或者花了大价钱微调了一个模型，结果业务需求已经变了。\n这不是技术问题，这是需求挖掘问题。一个简单的判断标准：你能否一句话说清楚「这个 AI 系统做成了，用户的什么行为会改变？」 如果你说不清楚，模型再强也没用。\n真实教训：hermes-home 里有一个 Agent 最早的设计是「帮我决定今天做什么」，上线后完全没人用。后来改成「帮我记录和管理待办事项」，用户每天在用。模型没换，Prompt 也没大幅改——换的是解决哪个问题。\n成本控制：不分层的架构跑不远 很多团队的做法是：选一个最强的模型，全量流量都走它。结果第一个月账单出来，团队傻眼了。\n分层的逻辑很简单：不是所有请求都值得用最好的模型处理。 80% 的请求可以用 DeepSeek V4 Pro 甚至 V4 Flash，15% 需要 GPT-5.6 Luna 级别，只有 5% 需要动用旗舰模型。\n实操层面，成本优化的手段远不止分层：\n语义缓存：重复查询命中缓存，彻底省掉模型调用 Prompt 压缩：精简历史记录和上下文，减少 token 消耗 Fallback 链：小模型先处理，处理不了再升级到大模型 输出长度控制：很多场景不需要完整输出，设置 max_tokens 能省一大半 2026 年的行业共识是：一个设计良好的分层架构，可以将 LLM 成本降低 30-50%，同时保持 95% 以上的用户体验不下降。\nAgent 工程：能力是 Engineering 出来的，不是模型送过来的 这是最容易被忽视、但也是真正的壁垒所在。\n从 GPT-3.5 到 GPT-5.6，模型能力在涨。但把一个 LLM 从「能回答问题」变成「能可靠地完成一个任务」，需要的是一整套工程能力——\nTool 设计：模型需要知道自己有什么工具可用，每个工具的输入输出是什么，什么时候该用哪个 Memory 管理：会话历史、长期记忆、知识库检索——怎么存、怎么查、怎么不爆上下文 错误处理：模型调用超时怎么办？工具返回异常怎么办？重试逻辑怎么写？ 可观测性：每次调用花了多少 token？哪个环节耗时最长？出错了怎么定位？ 多 Agent 协作：多个 Agent 之间怎么发现彼此的能力、怎么传递任务、怎么避免冲突 这些东西，换再强的模型也不会自动变好。它们需要工程投入。\n实战案例：一个跑在 DeepSeek 上的多 Agent 系统 hermes-home 是一个真实运行的多 Agent 系统，7 个 Agent 分布在云服务器和本地 Docker 中，通过 MCP 协议通信。这个系统 80% 的请求由 DeepSeek V4 Pro 处理。\n它能做什么？\n知识库 Agent 每天自动扫描 GitHub 新项目，整理入库 博客 Agent 从知识库拉取素材，写成 Hugo 文章并部署 待办 Agent 管理日常任务并定时提醒 决策 Agent 综合分析多个来源，给出结构化建议 所有这些任务，没有一个需要用到 Claude Fable 5 的全能力量。系统的瓶颈从来不在模型能力上——而是在 Tool 设计得够不够好、Memory 管理得够不够稳定、错误处理得够不够健壮。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR subgraph 模型层[\"模型层 (80% DeepSeek V4 Pro)\"] A[\"LLM 推理\"] end subgraph 工程层[\"工程层 (真正的壁垒)\"] B[\"Tool 系统\"] C[\"Memory 管理\"] D[\"错误处理\"] E[\"可观测性\"] end subgraph 应用层[\"应用层\"] F[\"7 个 Agent调研/写作/决策/待办\"] end A --\u003e B --\u003e C --\u003e D --\u003e E --\u003e F style 模型层 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 工程层 fill:#d1fae5,stroke:#059669,color:#064e3b style 应用层 fill:#fef3c7,stroke:#d97706,color:#78350f我花了大量时间优化的是中间这一层——Tool 的接口设计、Memory 的持久化策略、错误恢复逻辑、以及每次调用链路的可观测性。这些才是真正决定系统质量的因素。模型只要「够用」就行。\n下次有新模型发布的时候，先问自己三个问题：\n我的瓶颈真的是模型能力吗？ ——还是需求没挖对、成本没控好、工程没做扎实？ 换模型能解决我的什么问题？ ——它会让用户感知到显著提升吗？ 我的系统有足够好的可观测性吗？ ——如果你不知道每次调用花在哪、错在哪，换模型就是盲人摸象。 模型在飞速进步，这是好事。但 90% 的日常任务已经越过了「够用」的基准线。 真正的差距，从来不在模型本身。\n数据来源：OpenRouter API 实时定价、DeepSeek 官方发布公告（2026-04-24）、OpenAI GPT-5.6 官方公告（2026-07-09）、Anthropic Claude Fable 5 公告（2026-06-09）、RAND 企业 AI 项目元分析、AP News Kimi K3 报道。\n","date":"2026-07-17T00:00:00Z","image":"/images/you-dont-need-stronger-models-cover.png","permalink":"/post/you-dont-need-stronger-models/","title":"你其实不需要更强的模型"},{"content":"你有没有在夜间卫星地图上注意过一个细节——\n北上广深的灯光像超新星一样向外蔓延，长三角、珠三角连成一片光海。而越往内陆走，那些曾经亮着星星点点灯光的地方，正在逐年暗淡。\n这不是停电。这是一个正在发生的经济地理学事实：县城在消失。\n不是物理消失，而是从资源版图上被剥离。\n2026年的中国经济版图上，正在发生一场静默的断裂。一边是AI产业以举国之力狂奔，算力中心日夜不熄，海量资金涌入北上广深的核心研发圈；另一边是两千多个县城，机构缩编、学校撤并、年轻人外出不再回来。\n这两件事之间有一根隐蔽的管道——资源正在从县城被系统性抽走，注入大城市的新质生产力机器。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR subgraph 大城市[\"💡 大城市\"] direction TB L1[\"灯光如超新星向外蔓延\"] L2[\"AI产业聚集资源持续注入\"] end subgraph 县城[\"🌑 县城\"] direction TB R1[\"灯光逐年暗淡\"] R2[\"产业萎缩人口外流\"] end 大城市 --\u003e|\"虹吸资源\"| 县城 style 大城市 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 县城 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style L1 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style L2 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style R1 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style R2 fill:#fecaca,stroke:#dc2626,color:#7f1d1d两套政策，一张底牌 如果你把近三年中国关于AI的支持政策和关于县城的政策放在一起看，会发现这不是两套并行的体系，而是一次明确的资源重新配置。\nAI那一侧的关键词是「新型举国体制」「不惜代价」「资金狂奔」。从中央经济工作会议将新质生产力定为绝对核心驱动力，到东数西算工程、万亿产业大基金源源不断注入特定国家级算力枢纽——AI产业享受的是无限弹药的特权。\n县城那一侧的关键词则变成了「过紧日子」「底线思维」「战略降级」。中央要求人口小县推行机构改革，山西试点县党政机构被砍掉三分之一，事业编制缩减六成。国务院明确下文，要求中西部12个债务高风险省份严控甚至停建新建政府投资项目。县城的主体功能定位，被严格限制在了保证粮食安全、生态安全和服务农业农村上。\n翻译成大白话就是：你在后方待着，别添乱。\n这不是歧视，这是财政分配的现实逻辑。在经济高速增长的年代，国家可以一边给大城市输血，一边通过转移支付给县城修路。那时叫雨露均沾。但现在进入了存量博弈的时代，当国家需要集中几万亿甚至几十万亿去搞大模型、算力中心、黑灯工厂的时候，这笔钱只能通过压缩对县城的转移支付来筹措。\n国家对大城市AI产业的每一次巨额注资，本质上都是对县城资金的一次合法抽血。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR subgraph 中央[\"💠 中央财政\"] 财源[\"财政资金\"] end subgraph AI方向[\"🚀 AI/新质生产力\"] A1[\"万亿产业基金\"] A2[\"算力中心补贴\"] A3[\"芯片研发投入\"] end subgraph 县城方向[\"🏘️ 县城\"] B1[\"转移支付缩减\"] B2[\"机构编制压缩\"] B3[\"基建项目叫停\"] end 财源 --\u003e 优先分配 优先分配 --\u003e A1 优先分配 --\u003e A2 优先分配 --\u003e A3 优先分配 -.-\u003e|压缩| 县城方向 style A1 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A2 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A3 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B1 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style B2 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style B3 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style 财源 fill:#fef9c3,stroke:#ca8a04,color:#713f12雁行模型的失效——廉价劳动力不再值钱 过去三十年，中国县城之所以还能保住一点工业基础、还能缓解一部分人的就业，全靠一个产业转移机制。\n大城市是头雁。当北京上海深圳的土地和人口成本高到一定程度，那些需要大量劳动力的低端制造业，就会像大雁排队一样，先转移到内地的二线城市，最后落到广大的中西部县城。\n这个模型成立的唯一前提是劳动力成本的差异。县城招商引资最大的也是唯一的底牌就是：我这里人工便宜，只要两三千块钱，地皮几乎白送。\n但到了2026年，这张底牌正在失效。\nAI和智能机器人直接抹平了地理空间上的劳动力成本差异。试想一个场景：如果富士康或比亚迪要扩建产能，它是会把工厂建在距离深圳一千公里外、交通不便、配套落后的内地县城，去管理几万个每月拿三千块的工人，还是会在深圳周边的东莞或惠州，直接建一个全自动化的黑灯工厂？\n答案显而易见。\n搭载了最新AI视觉检测系统、多柔性机械臂的智能生产线全面普及之后，工厂可以24小时无休运转。这些机器人不用交社保、不需要住宿、没有情绪波动、良品率接近100%。即便精算到极致，在长三角珠三角布置一个机器人工位的综合成本，也一定会远远低于中西部县城一个最廉价工人的最低工资。\n既然机器的成本比县城的人工还要低，大企业为什么还要大费周折把产业链向内陆转移？它们完全可以把高度自动化的工厂留在核心城市周边——这里有世界级的港口、即刻可用的现代化物流、以及庞大的都市消费市场。\n这就是新质生产力带来的产业空间折叠。过去产业是分散的，因为人需要吃喝拉撒，需要分布在不同地理空间里。但在AI时代，产业是极度集权的——它不再依赖人，只依赖算力和电力。\n这不仅仅掐断了低端产业向中西部县城转移的生命线，更可怕的是，它还在反方向地绞杀县城原本仅存的初级加工制造业。在机器面前，县城最后的那点廉价劳动力优势，彻底归零了。\n资本不再需要廉价的人，资本需要的是廉价的算力和不知疲倦的机械臂。\n过去，产业沿着成本梯度向下转移：\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR A1(\"一线城市成本上升\") --\u003e|产业外迁| A2(\"二线城市承接转移\") A2 --\u003e|产业外迁| A3(\"县城靠廉价劳动力接盘\") style A1 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A2 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style A3 fill:#d1fae5,stroke:#059669,color:#064e3b现在，AI 让产业不再依赖人，工厂留在了城市圈：\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR B1(\"一线城市AI + 机器人\") --\u003e B2(\"黑灯工厂留在城市圈周边\") B2 -.-\u003e|县城被踢出| B3(❌) style B1 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style B2 fill:#d1fae5,stroke:#059669,color:#064e3b style B3 fill:#fecaca,stroke:#dc2626,color:#7f1d1dAI产业的三个要素，每一个都在排斥县城 传统工业时代，基础设施是可以下沉的。你修一条公路到县城，县城就能通车；你拉一根电线到县城，县城的灯就亮了。\n但AI产业的核心要素——算力、数据、算法人才——都具有极端空间排他性，每一个都在天然地抛弃县城。\n算力：建设一个顶级的大模型算力中心，不仅需要几百亿的硬件投入，还需要特高压专线电网来保证绝对不能断电，需要海量的水资源来进行液冷散热，需要极低延迟的骨干光纤网络。这些资源国家不可能平均分配给每一个县城——这不经济，也不符合规模报酬递增的规律。更残酷的是，在夏季用电高峰期，为了保证东部核心城市的AI算力中心不断电，中西部某些省份的传统高能耗产业会被行政命令要求错峰生产——让电于算。\n数据：高价值的数据来自高密度的数字场景——千万级的日活用户、亿级的交易记录、复杂的线上交互行为。县城没有这些。县城的数字活动是碎片化的、低频的、低价值的。数据不是均匀分布的，它是跟产业密度绑定的。\n人才：AI领域的顶尖人才天然向最前沿的公司和机构聚集。这不是因为\u0026quot;大城市好\u0026quot;，而是因为最好的同事、最多的GPU、最快的信息流动都在那里。县城走出去的高端人才，几乎不可能回流。\n更隐蔽也更残酷的是AI产业的乘数效应差异。\n传统工业时代，一个主导产业会拉动上下游一系列配套产业：汽车工业会拉动钢铁、橡胶、玻璃等上游产业，同时催生公路、加油站、维修店等下游产业。这些关联产业需要大量物理空间的劳动力，财富分配是发散型的，能够惠及县城和小镇。\n但人工智能产业是一个极度罕见的高密度、闭环、单轴产业。它对上下游的索取表现为完全的虹吸而非溢出。上游的算力与芯片抽干县城的廉价电能，中游的模型与算法虹吸县城走出去的全部高端人才，下游的应用则清洗县城本地的中产。\nAI产业链的运行逻辑，本质上就是 大城市开机、大厂收钱、县城失业、资金北上 的财富单向流动机器。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 传统[\"🏭 传统工业\"] --\u003e|\"发散型就业→惠及县城\"| A[\"县城受益\"] AI[\"🤖 AI产业\"] --\u003e|\"虹吸型资源→抽干县城\"| B[\"县城受损\"] style 传统 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style AI fill:#fecaca,stroke:#dc2626,color:#7f1d1d style A fill:#d1fae5,stroke:#059669,color:#064e3b style B fill:#fecaca,stroke:#dc2626,color:#7f1d1d%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 上游[\"⚡ 上游算力+芯片\"] --\u003e|电能| 县城[\"🏘️ 县城\"] 中游[\"🧠 中游模型+算法\"] --\u003e|人才| 县城 下游[\"📱 下游AI应用\"] --\u003e|岗位| 县城 上游 --\u003e 中游 --\u003e 下游 style 上游 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 中游 fill:#d1fae5,stroke:#059669,color:#064e3b style 下游 fill:#d1fae5,stroke:#059669,color:#064e3b style 县城 fill:#fecaca,stroke:#dc2626,color:#7f1d1d县城经济循环的崩塌，正在无声发生 在经济学中有一个核心概念叫「乘数效应」——一笔初始支出的变动，会通过经济体系内多轮循环消费，引发国民收入数倍于该初始支出的变动。\n在过去的健康状态下，县城的经济循环是这样的：\n外出打工的人寄钱回家 → 这笔钱发给县城的公务员、教师、工程老板 → 这批体制内中产和工程中产去饭店消费、去商场买衣服、去本地学校交学费 → 饭店老板和商场员工赚到了钱，再去菜市场买菜 → 养活县城底层的体力劳动者。\n每一块钱在县城内部流转多次，就能创造出数倍的社会总价值。这就是县城的生命线。\n但现在，三重打击正在让这个循环发生剧烈的负乘数效应。\n第一重打击：外出收入减少。 大城市工厂全面换上机器人，服务业开始裁减人工。县城流出人口在大城市能赚到的钱正在大幅缩减。寄回家的汇款少了，县城消费的第一笔源头就断了。\n第二重打击：工程老板群体消失。 顶层设计将资金集中于AI算力投资，县城传统基建项目被全面叫停。工程老板——这批县城最富有的群体——集体熄火了。没有他们的高消费，县城里高档餐厅、汽车维修店、建材市场这些依靠工程群体生存的产业，同步萎缩。\n第三重打击：体制内中产萎缩。 AI大模型正在替代县城的白领岗位——会计、审批人员、文员。再加上人口小县机构改革对编制的大幅缩减，县城里唯一具有持续消费能力的体制内和半体制中产，数量正在急剧缩减。\n当这三重打击叠加发生，负乘数效应就开始循环放大：\n中产不再消费 → 餐馆倒闭 → 菜贩失去收入 → 保洁、搬运工等底层零工也接不到活 → 所有人都在缩减开支 → 更多店铺关门 → 更多就业岗位消失。\n这就是AI和新质生产力背景下，发生在县城底层的、无声无息的经济链式死亡。\n财富被AI技术和国家战略以最高效的方式抽调、压缩、打包，送往了大城市。留给县城的，只是一潭死水。\n过去，县城的经济循环是正向的：\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR P1[\"外出收入汇回\"] --\u003e P2[\"体制内中产消费\"] P2 --\u003e P3[\"餐饮/零售/教育等服务\"] P3 -.-\u003e|循环放大| P1 style P1 fill:#d1fae5,stroke:#059669,color:#064e3b style P2 fill:#d1fae5,stroke:#059669,color:#064e3b style P3 fill:#d1fae5,stroke:#059669,color:#064e3b但现在，三重打击形成了负向的死亡螺旋：\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR N1[\"①外出收入减少②工程老板消失③体制内中产萎缩\"] --\u003e N2[\"中产消费降级\"] N2 --\u003e N3[\"餐馆倒闭、底层失业\"] N3 -.-\u003e|循环放大| N1 style N1 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style N2 fill:#fecaca,stroke:#dc2626,color:#7f1d1d style N3 fill:#fecaca,stroke:#dc2626,color:#7f1d1d三个不会说谎的硬指标 逻辑推理可能出错，但数据不会。这里有三个判断县城是否在衰落的硬指标。\n第一个指标：存贷差。\n这是判断一个地区有没有造血功能最准确的标志。近些年，许多中西部县城的银行数据呈现出诡异的特点：居民存款在上升，但当地企业贷款的发放量却在持续下跌。\n这意味着什么？\n老百姓把辛辛苦苦存下来的钱放进县城的银行，但银行因为当地没有能产生利润的新产业，根本不敢把钱贷给本地企业。更糟糕的是，县城银行的分支机构拿到这笔存款后，通过金融系统内部的资金调度，把这些钱上交给省行和总行。总行拿到钱后，转头就把贷款额度发放给了北京、上海、深圳搞芯片、大模型、自动化工厂的科创巨头。\n结论很清楚：县城的金融系统已经完全异化为一台巨大的资金抽水机。 县城老百姓的血汗钱通过金融工具，转化成了大城市AI公司的研发弹药。县城在用自己的血，养活大城市的再生长。\n第二个指标：小学生在校人数。\n房地产研究员和人口经济学家判断一个城市未来五年基本面的最常用指标，就是小学生在校人数。因为这个数据最真实——学籍很难造假，家长也必须跟着孩子走。\n从2024到2026年，绝大多数普通县城的小学生在校人数出现了肉眼可见的下降。背后的推力一方面是总和生育率下降，但更致命的是公共资源的逆向虹吸。国家把钱砸向新基建，县城就没钱了，于是县里开始强制撤并乡村和普通公办学校。但凡有一点认知和购买力的家长，为了不让孩子在教育上掉队，把孩子送往地级市甚至省会去读书，并在那里买房租房。\n在经济学中，孩子是一个家庭资源投射的核心。县城学龄人口的断崖式下跌，意味着当地最有活力的中青年群体以及家庭的核心消费，正在对县城进行一场不可逆的物理逃离。\n第三个指标：夜间灯光指数。\n空间遥感经济团队通过对比近五年中国夜间灯光的卫星影像，发现了一个极度撕裂的趋势：北上广深、长三角、中三角等核心都市圈的灯光不仅亮度在增加，而且快速向外蔓延；而那些远离核心都市圈的中西部普通县城，其夜间灯光强度出现了显著的暗淡和收缩。\n这是能源重配的视觉证据。大城市的万家灯火如同夜空中的超新星，吞噬着海量电力，点亮了数字经济的版图。而县城，因为传统高能耗产业被打压、工厂停工，不仅厂房黑了，连路灯都在被节能改造。\n三个指标叠在一起，就是一份对县城衰落的判决书。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 存贷差[\"📊 存贷差存款流向大城市企业\"] ---\u003e 结论 学生数[\"📚 在校生数学龄人口物理逃离\"] ---\u003e 结论[\"📋 判决书：县城正在衰落\"] 灯光[\"🌙 夜间灯光县城逐年变暗\"] ---\u003e 结论 style 存贷差 fill:#fef3c7,stroke:#d97706,color:#78350f style 学生数 fill:#fef3c7,stroke:#d97706,color:#78350f style 灯光 fill:#fef3c7,stroke:#d97706,color:#78350f style 结论 fill:#fecaca,stroke:#dc2626,color:#7f1d1d个体怎么办：不是恐慌，是规划 前面的分析不是让人绝望的。看清楚了趋势，反而可以开始行动。\n在这个以新质生产力和算力集权为核心的大背景下，那些没有特定禀赋的普通县城，在宏观的战略沙盘里已经被正式定位为一种社会缓冲带——只需要按惯性待在那里，维持最低限度的消耗，不给前线正在打科技战的大城市添乱。\n但个体完全可以主动选择。\n第一条路：境内移民。\n这不是今天收拾行李明天冲去北上广深。而是把你的人生坐标从\u0026quot;我出生在哪里\u0026quot;慢慢改成\u0026quot;哪里更值得我长期配置自己\u0026quot;。\n一个三年的计划：\n阶段 时间 任务 信息迁移 第一年 研究目标城市的招聘、房租、工资、产业、学校、医疗、生活成本 技能迁移 第二年 确保你的技能在目标城市能换到钱——如果不够，就补技能：编程、运营、设计、销售、护理、维修、跨境电商、短视频运营，至少选一个能在城市里交易的技能 身份迁移 第三年 社保、居住证、工作、租房、孩子教育、父母医疗，一步一步往目标城市挪 你不需要去最贵的一线城市。强二线和核心都市圈——杭州、成都、武汉、苏州、长沙——完全够用。关键不是买不买房，而是先问自己能不能在那边活下来。先租房、先找工作、先看行业、先建立人脉，先让自己在那个城市有一个落脚点。\n普通人的迁移不是浪漫旅行，而是项目管理。你要有时间表，有预算表，有至少六个月的备用金，有一条撤退路线。\n第二条路：不移民但接入。\n如果你因为各种原因不能或不想离开县城，那么必须建立跨空间的货币获取能力——通过互联网和数字技术，从大城市的高势能网络中赚钱，而不是在县城里跟存量做内卷。\n远程工作、自由职业、AI工具辅助的个体产出——这些东西不依赖你所在的地理位置，只依赖你接入网络的能力。如果你懂编程，可以远程接外包；如果你懂内容，可以做出海；如果你懂投流，可以在线上做生意。县城的生活成本低，只要你的收入来自大城市的渠道，你反而有套利空间。\n这两条路并不矛盾，甚至可以并行。境内移民解决的是长期的安全边际，跨空间赚钱解决的是当下的收入来源。\n%%{init: {'theme':'neutral', 'fontFamily':'monospace'}}%% flowchart LR 起点[\"你的选择\"] --\u003e 判断{\"离开县城？\"} 判断 --\u003e|\"是\"| 移民[\"📍 境内移民三年计划：信息→技能→身份\"] 判断 --\u003e|\"否\"| 接入[\"🌐 跨空间赚钱远程工作/数字产出\"] style 起点 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style 判断 fill:#fef9c3,stroke:#ca8a04,color:#713f12 style 移民 fill:#dbeafe,stroke:#2563eb,color:#1e3a5f style 接入 fill:#d1fae5,stroke:#059669,color:#064e3b 回到开头的卫星地图。\n当一座城市的灯光在卫星图上逐年变暗，它不是消失了——它说明这座城市在资源版图上，正在变成\u0026quot;不被看见\u0026quot;的地方。\n你不能决定资源怎么分配，但你可以决定自己往哪里移动。越早开始规划，选择权越大。等到所有人都想走的时候，你才发现自己没有技能、没有存款、没有社保、没有信息渠道——那才是真正的困境。\n这件事情发生之前看起来离你很远，发生之后你才会知道：原来你所在的位置，决定了别人会不会及时看见你。\n这不是鸡汤，这是生存。\n本文观点来自对两期B站视频内容的梳理与分析。\n","date":"2026-07-17T00:00:00Z","image":"/images/county-disappearing-cover.png","permalink":"/post/county-disappearing/","title":"县城正在消失，而你还在犹豫要不要走"},{"content":"你有没有想过一个问题——\nLLM 训练时读了几万亿字的互联网数据，几乎什么都知道。但它不知道你的业务数据——你的产品手册、售后政策、内部文档、客服对话记录。这些它没读过。\n怎么让它知道该知道的？\n这就是 RAG 要解决的问题。这篇文章从为什么需要 RAG 开始，一直讲到它的完整架构和最容易翻车的地方。\nLLM 什么都知道，但不知道你的业务 LLM 有两个先天缺陷，在实际落地中很难绕过去。\n缺陷 表现 影响 知识截止 训练数据停在某个时间点 问最新政策、实时数据，它不知道 不知道私有数据 没读过你的内部文档、产品手册、客服记录 业务场景下基本不可用 怎么解决？目前有三条路可以走。\n方案一：微调（Fine-tuning） 拿业务数据继续训练模型，让它把新知识学进去。\n听起来最直接，但实际操作成本很高。你需要准备高质量的训练数据、处理算力资源，而且每次业务文档更新了，又得重新训一遍。更麻烦的是，微调可能会破坏模型原有的通用能力——模型学会了你的业务术语，但写邮件的能力反而变差了。\n方案二：塞长上下文（Long Context） 把所有资料都塞进 Prompt 里，让模型自己翻。\n实现最简单，不用动模型。现在有些模型支持 128K、1M 甚至更长的上下文，看起来够用。但实际效果没那么理想：一是 Token 成本会随着内容量线性增长；二是内容太多之后，模型很难从海量信息中找到准确的那一段——这就是著名的\u0026quot;大海捞针\u0026quot;问题。上下文再长，检索精度是会下降的。\n方案三：RAG（检索增强生成） 每次提问的时候，先去知识库里检索相关内容，拼到 Prompt 里，再让 LLM 基于这些材料回答。\nRAG 是目前最主流的方案。原因很实在：\n成本低：不需要重新训练模型，省了算力 更新快：知识库加个文档就行，不需要重训 可追溯：LLM 是看着你给的资料回答的，能知道它引用的是哪份文档 幻觉少：基于具体材料生成，比凭空回答可靠得多 当然，RAG 也有自己的问题——它依赖检索质量。如果检索到的内容不对或者不完整，LLM 材料再好也回答不对。后面会详细讲这个。\nRAG 不是搜索引擎——它和搜索引擎的区别 很多人第一次接触 RAG 的反应是：这不就是搜索引擎吗？用户搜关键词，系统返回匹配的文档——有什么新鲜的？\n这个理解对了一半，错的一半恰好是 RAG 的核心。\n搜索引擎（ES/BM25）怎么工作 你搜一个关键词，搜索引擎在你的文档库里找到包含这个词的所有文档，按匹配度排序返回。它做的是关键词精确匹配。\n比如说你搜\u0026quot;2025年Q3营收\u0026quot;，它能找到包含这几个词的那份财报。但如果你问\u0026quot;去年第三季度营收怎么样？\u0026quot;，它可能找不到——因为文档里写的是\u0026quot;2025年Q3营收同比增长15.3%\u0026quot;，关键词对不上。\n%%{init: {'theme':'neutral'}}%% graph LR A[\"搜：去年第三季度营收怎么样？\"] --\u003e B[ES 关键词匹配] B --\u003e C[\"找到「去年」「第三」「季度」「营收」\"] C --\u003e D[❌ 文档写的是「Q3营收同比增长15.3%」关键词对不上，找不到]这是一直以来搜索引擎的运作方式。在\u0026quot;搜文档、找文件\u0026quot;的场景下非常好用，但在\u0026quot;回答问题\u0026quot;的场景下就显得力不从心。\nRAG（向量检索）怎么工作 RAG 不一样。它先把你的文档库转成一组向量（可以理解成一组数字坐标），用户提问时也把问题转成向量，然后计算问题向量和文档向量之间的距离。距离越近，说明内容越相关。\n%%{init: {'theme':'neutral'}}%% graph LR A[\"同样的问题：去年第三季度营收怎么样？\"] --\u003e B[RAG 向量检索] B --\u003e C[把问题和文档都转成向量计算语义距离] C --\u003e D[✅ 匹配到「Q3营收同比增长15.3%」语义相近，找到了]\u0026ldquo;去年第三季度营收怎么样？\u0026ldquo;和\u0026quot;2025年Q3营收同比增长15.3%\u0026ldquo;在字面上没有共同的关键词，但在语义上是同一件事。向量检索能捕捉到这种语义上的相近。\n维度 搜索引擎（ES/BM25） RAG（向量检索） 匹配方式 关键词精确匹配 语义匹配 搜\u0026quot;苹果\u0026rdquo; 返回含\u0026quot;苹果\u0026quot;二字的所有文档 能区分你在问水果还是手机品牌 输出 文档列表 相关内容 + LLM 生成的自然语言回答 适合场景 搜文档、找文件 回答问题、知识问答 局限性 同义词、近义词不识别 对数字/专有名词匹配不如精确搜索 所以搜索引擎和 RAG 不是替代关系，而是互补关系。生产环境最常用的方案是混合检索：向量检索负责语义匹配，BM25 负责精确匹配，两者互补。\n但语义检索有一个前提条件：内容必须被正确地理解和切分。这就引出了下一个问题。\n一条文档在 RAG 系统中的完整旅程 语义匹配的效果，很大程度上取决于文档在入库时被处理成了什么样子。一条文档从上传到最终被 LLM 用来回答问题，中间经历了一整套加工管道。\nRAG 系统完整管道：入库阶段 → 检索阶段（点击图片查看大图）\n入库阶段：让机器能\u0026quot;读懂\u0026quot;文档 环节 做什么 为什么重要 文档解析 把 PDF、Word、HTML 等格式转成纯文本 PDF 看着是文字，实际是一堆坐标指令。解析不好，后面的所有环节都白费 文本清洗 去噪、去重、格式归一化 把乱码、多余的空格、无关的页眉页脚清掉 分块 把长文本切成语义完整的片段 不能整篇检索——太大了检索不准，太小了语境不够 向量化 用 Embedding 模型把文本片段转成向量 向量是语义匹配的基础 存入向量库 把向量和原文存入向量数据库 供后续检索使用 检索阶段：找到相关内容并让 LLM 回答 环节 做什么 问题向量化 把用户提问也转成向量 向量检索 在向量库里找距离最近的文档片段 重排序 对检索结果重新精排，提升 Top 结果的准确率 拼入 Prompt 把检索到的内容拼到 Prompt 里，加上指令让 LLM 基于材料回答 为什么检索质量是关键 整个管道里，最核心的瓶颈不是 LLM 本身，而是检索质量。\nAndrew Ng 在一个分享里提过一个数据：优化 RAG 系统时，检索质量的提升对最终答案准确率的影响远大于模型本身的升级。换句话说，用更强的模型不能弥补检索结果差的问题——模型再聪明，材料给错了也答不对。\n而检索质量又依赖于前面的入库环节：文档解析是否正确、分块是否合理、向量化是否准确。这是一个完整的链路，短板出在任何一环都会影响最终效果。\n最容易在哪里翻车？ 把各个环节按翻车概率排序，前三个是：\n1. 文档解析——最大的坑 你传上去的文档是 PDF。PDF 看着是文本，但本质上是一堆坐标指令的集合——\u0026ldquo;在坐标 (x,y) 处渲染字符 A，在 (x+5,y) 处渲染字符 B\u0026rdquo;。它并不知道什么是段落、什么是表格、什么是页眉。\n遇到以下几种 PDF，解析很容易翻车：\nPDF 类型 难度 翻车方式 数字原生 PDF ⭐ 简单 基本没问题 扫描件 PDF（图片） ⭐⭐⭐ 中等 需要 OCR，识别率取决于清晰度 双栏/多栏排版 ⭐⭐⭐⭐ 困难 阅读顺序恢复错误，左右栏混在一起 含表格 PDF ⭐⭐⭐⭐⭐ 极难 无线框表格、合并单元格、跨页表格，很容易解析错位 含公式 PDF ⭐⭐⭐⭐⭐ 极难 LaTeX 公式、化学结构式，专用模型才能处理好 2. 分块——切得不合适 解析完的文本不能整篇扔进去检索。整篇检索的问题：一篇文档可能几千字，但用户问的只是其中一小段。整篇匹配会让不相关的内容稀释相关内容的信号。\n但切得太细也不行。比如只切一两句话——检索时可能命中了，但上下文缺失，LLM 看了也不知道前因后果。\n这就是分块的核心矛盾：检索精度 vs 上下文完整性。切小了检索精准但语境不足，切大了语境完整但检索不精准。\n实际生产中有好几种分块策略来解决这个矛盾——按固定大小切、按语义边界切、按文档结构切、父子分块等等。每种策略各有优缺点，选型取决于你的文档类型和场景。\n3. 检索——精确匹配 vs 语义匹配的盲区 即使解析和分块都做好了，检索环节仍然可能翻车。典型场景：\n找不到相关内容：你的文档里确实有答案，但向量检索没把它排到前面 找到太多噪音：检索返回了大量不太相关的内容，稀释了有效信号 数字/专有名词匹配弱：向量检索对\u0026quot;Q3-2025营收同比增长率15.3%\u0026ldquo;这种精确表达不如关键词搜索 这也是为什么生产环境通常用混合检索——语义 + 关键词组合，取长补短。\n回头看看 把整篇文章的问题串起来：\nLLM 不知道你的业务数据 → 三种方案对比，RAG 最实用（成本低、更新快、可追溯） → RAG 不是搜索引擎，是语义匹配 → 但语义匹配的前提是内容被正确加工 → 入库阶段：解析 → 清洗 → 分块 → 向量化 → 存储 → 检索阶段：检索 → 重排序 → 拼入 Prompt → 生成 → 最容易翻车的地方：解析、分块、检索 RAG 看起来简单——\u0026ldquo;查资料再回答\u0026rdquo;——但做了才发现，每一步都有坑。理解了这个全貌，后续不管是选型还是排查问题，都知道问题可能出在哪一环。\n参考资料\nAndrew Ng, Building RAG Agents with LLMs, DeepLearning.AI, 2025 — 检索质量对 RAG 效果影响的分享 Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, arXiv:2005.11401, 2020 — RAG 论文原文 ","date":"2026-07-10T00:00:00Z","image":"/images/rag-overview-cover.png","permalink":"/post/rag-overview/","title":"RAG 是什么？从一条文档的完整旅程说起"},{"content":"你有没有想过一个问题——\nRAG 的核心是「语义匹配」：你说「去年第三季度营收怎么样」，它能找到文档里写的「Q3 营收同比增长 15.3%」。字面不一样，但意思一样。\n这到底是怎么做到的？一段文字怎么变成一个「向量」？为什么两个向量离得近就代表意思相近？\n这篇文章从最基础的概念讲起，不涉及复杂的数学公式。\n人能判断语义相似度，机器也能 先想一个简单的问题：人和人之间怎么判断两句话意思差不多？\n「今天天气真好」和「今天阳光明媚」——你一看就知道它们说的是一回事。\n背后的过程很复杂（你用了多年的语言经验、对词汇的理解、对语境的把握），但直觉上很简单：你在大脑里给这两句话各自建立了一个「语义位置」，发现它们靠得很近。\n机器做同样的事，只是用的方法不一样——它用向量来表示语义位置。\n向量是什么？一组数字而已 向量听起来很高深，其实就是一组数字。\n[0.52, -0.13, 0.87, -0.42, 0.11, ...] 你可以把向量想象成语义空间中的一个坐标点。\n比如我们想象一个二维的语义空间，每一段话都对应一个坐标点：\n「今天天气真好」→ 坐标 (0.9, 0.3) 「今天阳光明媚」→ 坐标 (0.8, 0.4) 「苹果很好吃」 → 坐标 (0.1, 0.2) 在语义空间里，「今天天气真好」和「今天阳光明媚」靠得近（意思相近），和「苹果很好吃」离得远（不相关）。\n二维语义空间示意：意思相近的文本在空间中靠得近\n当然，现实中的语义向量不是两维，而是几百甚至几千维——但原理一样。两个向量在空间中的距离越近，代表它们的意思越相近。\n语义从哪来：Embedding 模型怎么训练的 刚才的水果例子是我手动编的维度（甜度、大小）。但在真实场景中，不可能有人来给每个词标注「甜度 = 0.9」这种数据。\n那向量的数值从哪来？答案是 Embedding 模型自己学出来的。\n训练方法的核心思路很简单：出现在相似语境中的词，应该有相似的向量。\n一个经典的例子：\n「国王」和「女王」经常出现在类似的句子里（「__ 统治着国家」「__ 戴着一顶王冠」）。所以它们的向量应该离得近。\n「国王」和「苹果」很少出现在同一个语境里。所以它们的向量应该离得远。\n训练时，模型会读海量的文本，对每个词做一个调整：如果两个词经常出现在相似的上下文里，就把它们的向量拉近一点；如果很少同时出现，就把它们推远一点。\n经过无数次这样的调整，模型最终学会了一个语义空间——意思相近的词，在空间里靠得近；意思不相关的词，离得远。\n这一步就是 Embedding 模型做的事。你给它输入一段文本，它返回一组向量。\n%%{init: {'theme':'neutral'}}%% graph LR A[\"输入文本去年第三季度赚了多少\"] --\u003e B[Embedding 模型] B --\u003e C[\"输出向量[0.23, -0.56, 0.91, ...]\"] style A fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style B fill:#d1fae5,stroke:#10b981,color:#064e3b style C fill:#fce7f3,stroke:#ec4899,color:#831843如何判断两个向量离得近 有了向量之后，怎么计算两段文字的相似度？\n最常用的方法叫余弦相似度（Cosine Similarity）。你不需要记住公式，只要理解它的直觉：\n两个向量在空间中有一个夹角。夹角越小，说明方向越一致，内容越相似。\n完全相同的文本 → 夹角 0°，相似度 = 1 完全不相关的文本 → 夹角 90°，相似度 ≈ 0 意思相反的文本 → 夹角 180°，相似度 = -1 %%{init: {'theme':'neutral'}}%% graph LR A[\"文本A: 今天天气真好\"] --\u003e C[Embedding 模型] B[\"文本B: 今天阳光明媚\"] --\u003e C C --\u003e D[\"向量A: [0.8, 0.5, ...]向量B: [0.7, 0.6, ...]\"] D --\u003e E[\"余弦相似度 = 0.92✅ 意思相近\"] style A fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style B fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style C fill:#d1fae5,stroke:#10b981,color:#064e3b style D fill:#fce7f3,stroke:#ec4899,color:#831843 style E fill:#d1fae5,stroke:#10b981,color:#064e3b回到 RAG：向量检索就是这样工作的 RAG 的检索过程，就是把上面的逻辑倒过来：\n入库时：把每篇文档的每个片段都通过 Embedding 模型转成向量，存进向量数据库 查询时：把用户的问题也转成向量 匹配时：计算问题向量和所有文档向量的余弦相似度，找出最接近的那几个片段 用户问题：去年第三季度我们赚了多少钱？ │ ▼ Embedding 模型 │ [0.23, -0.56, 0.91, ...] │ ▼ 跟库里所有文档向量算相似度 │ 文档中匹配到的内容：Q3营收同比增长15.3%（相似度 0.87） 去年全年营收1.2亿元（相似度 0.45） 公司成立于2018年（相似度 0.12） │ ▼ 取 Top-K，拼入 Prompt │ LLM 基于这些内容生成回答 向量检索不只是「找到包含关键词的文档」，而是「找到意思最相近的内容」。这也解释了为什么在 RAG 中，分块和向量化的质量决定了检索的天花板——如果文档切得不好、向量化得不准确，后面用多好的 LLM 也弥补不了。\n参考资料\nMikolov et al., Efficient Estimation of Word Representations in Vector Space, arXiv:1301.3781, 2013 — Word2Vec，向量化技术的奠基论文 Reimers \u0026amp; Gurevych, Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks, EMNLP 2019 — 现代 Embedding 模型的基础架构 Datawhale, hello-agents, 第八章 记忆与检索, https://github.com/datawhalechina/hello-agents — 从认知科学到向量检索的实践教程 ","date":"2026-07-10T00:00:00Z","image":"/images/embedding-principle-cover.png","permalink":"/post/embedding-principle/","title":"向量化原理——为什么向量能算语义相似度"},{"content":"如果说 2024 年是\u0026quot;AI 聊天机器人\u0026quot;普及的一年，那么 2025-2026 年则是\u0026quot;AI Agent\u0026quot;从概念走向工程的一年。Claude Code、OpenAI Codex、Manus、Cline……越来越多的产品不再满足于\u0026quot;你问我答\u0026quot;，而是主动帮你完成任务——读代码、改文件、跑测试、甚至自己写一篇博客。\n但 Agent 到底是什么？它和普通的 LLM 对话有什么区别？这篇文章将从程序员熟悉的概念出发，系统地梳理 AI Agent 的核心概念、关键特性和基础架构模式。\n一、从对话到自主：Agent 的核心定义 最简单的定义来自 LangChain：\nAgent = Model + Harness\n翻译成程序员能理解的语言：Agent 不是更聪明的 LLM，而是把 LLM 装进了一个有工具、有记忆、有控制循环的\u0026quot;运行时环境\u0026quot;里。\n你可以把 LLM 想象成一个天才程序员，但你只能跟他口头交流——他再聪明，不动手也写不了代码。Agent 就是给了这个天才一台电脑、一套工具链、和一份\u0026quot;你自己想办法搞定\u0026quot;的指令。\nAnthropic 的 Barry Zhang 有一个更简洁的说法：\nAgent = 在 Loop 中使用工具的模型\n关键的区别在于：\n特性 普通 LLM 对话 Agent 交互方式 一问一答 给定目标，自主行动 工具使用 不能 可以调 API、读写文件、执行命令 记忆 上下文窗口（易失） 持久化记忆（跨会话） 决策 用户决定下一步 自主决定下一步 错误处理 用户发现并纠正 自行检测和重试 二、Agent 的五个关键特性 从工程角度看，一个完整的 Agent 系统通常具备以下五个特性：\n1. 工具调用（Tool-use） Agent 不只\u0026quot;说\u0026quot;，还能\u0026quot;做\u0026quot;。通过工具调用操作外部系统——读文件、搜网页、发 API、执行命令。\n💡 类比：工具的接口就像微服务的 API endpoint，有 schema、参数、返回值。Agent 根据任务自主选择调哪个\u0026quot;端点\u0026quot;。\n2. 记忆（Memory） Agent 需要跨会话记住你的偏好、工作环境、学到的经验。记忆分为多个层级：\n文件层：MEMORY.md、USER.md 等结构化文件，持久化存储 语义层：向量检索，用于快速召回相关历史 上下文层：当前会话窗口，易失但高效 💡 类比：上下文窗口是 Redis（缓存，易失），文件系统是 PostgreSQL（持久存储）。\n3. 规划（Planning） 复杂任务需要拆解。Agent 先制定计划（如 plan.md），再分步执行——每一步完成后再决定下一步做什么。\n💡 类比：就像你写代码之前先画架构图、列 TODO 清单。Agent 也需要先规划再行动。\n4. 自主决策（Autonomous Decision） 这是 Agent 和\u0026quot;带工具的聊天机器人\u0026quot;的本质区别。Agent 自主决定调哪个工具、用什么参数、是否重试。\n关键设计原则：Error-as-Data。工具失败时，错误信息作为数据回写给 LLM，而不是抛异常——Agent 自己决定是重试还是换工具。\n5. 验证与反馈（Verification） Agent 需要自己检查结果是否正确。没有验证的 Agent 就像没有测试用例的 CI/CD 流水线——跑得再快也不知道对不对。\nBoris Cherny（Claude Code 负责人）的一条核心经验：给 LLM 自我验证的能力，输出质量能提升 2-3 倍。\n三、四层工程模型：理解 Agent 系统的骨架 这是理解 Agent 技术栈最清晰的可视化框架，由浅入深分为四层：\nL4 Loop Engineering —— 设计替你跑 Agent 的系统 L3 Harness Engineering —— 怎么把 Agent 装好 L2 Context Engineering —— 怎么给 Agent 喂对信息 L1 Prompt Engineering —— 怎么把话说清楚 ⚠️ 注意：四层是嵌套关系而非替代关系。L3 不覆盖 L2，而是在它上面叠加。\nL1: Prompt Engineering 最基本的层次。研究\u0026quot;怎么写指令\u0026quot;能让 LLM 给出更好的回答。包括角色设定、示例引导、思维链（Chain-of-Thought）等技巧。\n程序员熟悉的起点：就像写一个好的函数注释和调用说明。不同之处在于 Prompt 的\u0026quot;读者\u0026quot;是 LLM，它比编译器宽容得多，但也会产生你意想不到的输出。\nL2: Context Engineering 这一层关注\u0026quot;给 LLM 什么信息\u0026quot;。Andrej Karpathy 提出的概念——长上下文不是银弹，关键是上下文的质量和结构。\n核心实践包括：\n渐进性披露：先给概要，逐步展开细节，避免信息过载 上下文窗口管理：当上下文超长时，如何压缩、优先排序、丢弃 结构化上下文：用 XML、JSON 等格式组织信息，让 LLM 更容易解析 💡 类比：Prompt Engineering 是\u0026quot;怎么问\u0026quot;，Context Engineering 是\u0026quot;给什么参考材料\u0026quot;。\nL3: Harness Engineering 这是 Agent 工程的核心创新层。Mitchell Hashimoto（Harness Engineering 提出者）将其定义为：\n围绕 LLM 构建一个\u0026quot;操作系统\u0026quot;——包括工具系统、权限控制、上下文管理、反馈回路、可观测性。\nHarness 的四条铁律：\n工具签名即文档 — 每个工具的名称、参数、返回值的设计本身就是使用说明 结果必须可验证 — 每一步的输出需要有明确的验证标准 错误不可沉默 — 所有失败必须可见并结构化回传（Error-as-Data） 渐进性披露 — 不要让 Agent 同时面对所有信息，按需呈现 LangChain 的 Deep Agents 项目做过一个令人印象深刻的实验：仅仅优化 Harness（不改模型），Terminal Bench 2.0 排名从第 30 位直接跃升至第 5 位。 这证明了瓶颈往往不在模型本身，而在\u0026quot;怎么装\u0026quot;。\nL4: Loop Engineering 最高层次，研究\u0026quot;自动触发 Agent 的循环系统\u0026quot;。不是你自己跑一次 Agent，而是系统定期或按事件自动启动 Agent。\nBoris Cherny 的实践是代表性案例——他不再\u0026quot;提示\u0026quot;Claude，而是\u0026quot;写循环\u0026quot;（I don\u0026rsquo;t prompt Claude anymore. I write loops.）。\n典型的 Loop 模式：\n事件驱动：文件变更 → 自动触发代码审查 定时任务：每日自动抓取信息、生成摘要 Pipeline：多个 Agent 接力完成复杂工作流 四、基础架构模式 4.1 ReAct Loop（核心循环） ReAct（Reasoning + Acting）是所有现代 Agent 的基座模式。它的执行循环可以用一行伪代码概括：\nwhile (task_not_complete) { think() // 推理：当前状态 + 下一步做什么 act() // 行动：调用工具或生成输出 observe() // 观察：收集执行结果 } Manus、Cline、Claude Code 都是 ReAct Loop 的具体实现。不管上层叠了多少层 Harness 或 Loop，最底层永远是 observe-think-act 这个基本循环。\n💡 类比：一个 while(true) { think → act → observe } 的事件循环。Agent 每次迭代都自主决定是调用工具还是给出最终答案。\n4.2 Plan-and-Execute Agent 先制定完整计划，再分步执行。典型流程：\nInit 阶段：分析任务，生成 plan.md，分解为子步骤 Exec 阶段：按计划逐步执行，每步完成后更新进度 Check 阶段：验证结果是否符合预期，必要时回退或修正 这种模式的优点是任务可追溯、可断点续传。缺点是灵活性不如纯 ReAct——当实际情况偏离计划时，需要 Agent 具备\u0026quot;重新规划\u0026quot;的能力。\n4.3 Tool-use 模式 专门优化工具调用的架构，关键要点：\n工具名用动词短语：parse_resume、score_match，而不是 resume_tool、match 参数含正反面说明：告诉 LLM 什么情况下用什么参数 返回值结构稳定：格式可预测，方便 LLM 解析 工具隔离：每个 Sub-Agent 只装它真正需要的工具 4.4 MCP 协议 MCP（Model Context Protocol）是 Anthropic 提出的工具层标准协议。可以理解为 Agent 世界的 USB 协议——任何工具只要实现 MCP，就能即插即用。\n\u0026ldquo;OneAgent + MCPs\u0026rdquo; 范式正在成为趋势：用一个统一的基础 Agent，通过不同领域的 MCP 服务器扩展能力，代替传统的\u0026quot;每个场景一个独立 Agent\u0026quot;的模式。\n五、关键术语速查表 术语 一句话解释 程序员类比 Agent 能自主决定下一步做什么的 LLM 一个有 main loop 的微服务，每次迭代自己选路由 LLM Agent 的\u0026quot;大脑\u0026quot;，负责推理和决策 一个超级强大的 if-else 引擎，输入是自然语言 Tool Agent 操作外部世界的接口 微服务的 API endpoint，有 schema、参数、返回值 Memory 跨会话持久化知识 数据库（持久层）+ 缓存（上下文窗口） ReAct Loop 基础执行循环 while(true) { think → act → observe } Harness 把 Agent 装起来的\u0026quot;操作系统\u0026quot; 相比\u0026quot;怎么写\u0026quot;（Prompt），优化\u0026quot;怎么装\u0026quot; Loop 自动触发 Agent 的循环系统 cron + event-driven 架构 Workspace Agent 的\u0026quot;当前工作目录\u0026quot; 不是变量，是 git 仓库——每一步可回放、可审计 Verifier 检查 Agent 输出是否正确 断言之于测试 Skill 可复用的知识包 插件系统 + 懒加载库 MCP 工具层的标准协议 Agent 世界的 USB 协议 六、从哪里开始？ 如果你是一个有编程基础的程序员，想开始实践 Agent 工程，以下资源值得关注：\n一手信息源（英文，需网络环境） 来源 推荐理由 Lilian Weng, \u0026ldquo;LLM Powered Autonomous Agents\u0026rdquo; Agent 入门圣经，系统讲工具调用、记忆、规划三大支柱 Anthropic, \u0026ldquo;Build Effective Agents\u0026rdquo; 官方最佳实践，简洁实用 Mitchell Hashimoto 博客 Harness Engineering 原创者 Boris Cherny, howborisusesclaudecode.com Claude Code 之父，loop 实战 Addy Osmani Substack Loop Engineering 提出者 国内可访问的中文资料 文章 来源 推荐角度 Agent Harness 工程实践 阿里云开发者 Harness 定义 + 四条铁律 Harness 工程之道：Skill 原理与最佳实践 阿里云开发者 渐进性披露、Skill 目录结构 Loop Engineering 四层模型 腾讯云开发者 四层嵌套框架（本文骨架来源） OneAgent + MCPs 范式 阿里云开发者 Agent 发展四阶段模型 Boris Cherny 的 /loop 实战 歪脖抠腚 验证先行、CLAUDE.md 知识积累 七、结语 回到最核心的那句话：Agent = Model + Harness。\n模型本身的能力固然重要，但工程落地的瓶颈往往不在模型够不够聪明，而在有没有把它装好。Prompt Engineering 只是起点，往上还有 Context Engineering（喂对信息）、Harness Engineering（装好系统）、Loop Engineering（自动化循环）——每一层都是值得深入的方向。\n从今天开始，不妨试着用 Agent 的思维来思考问题：不是\u0026quot;让 AI 帮我回答这个问题\u0026quot;，而是\u0026quot;给 AI 一个目标、一套工具、一个自主决策的环境，让它自己去搞定\u0026quot;。\n这才是 Agent 时代真正的编程范式转变。\n","date":"2026-07-05T00:00:00Z","image":"/images/ai-agent-introduction-cover.png","permalink":"/post/ai-agent-introduction/","title":"AI Agent 入门：程序员视角的概念与实践"},{"content":" Agent 不是“更强的提示词”，也不是把模型接上终端。它是一组逐层补齐的能力：推理、行动、工具使用、反馈、可复用经验，以及适配真实环境的接口。\n这条线从 CoT 延伸到 SWE-agent。把六篇论文按“上一步已经解决了什么，人们接下来还想要什么”来读，名词表就会变成一张工程问题地图。\n先给结论：六篇论文，六个能力缺口 论文 它回答的问题 留给工程的能力 CoT 复杂问题如何不只猜最终答案？ 显式中间推理 ReAct 推理如何接触外部环境？ Thought → Action → Observation 循环 Toolformer 模型如何知道何时、怎样用 API？ 工具使用的学习问题 Reflexion 一次失败如何影响下一次尝试？ 语言化反馈进入后续上下文 Voyager 成功行为如何不必每次重学？ 可执行 Skill 的积累与复用 SWE-agent 代码任务环境怎样才适合 Agent 使用？ 面向 Agent 的软件接口（ACI） 它们不是六个互相替代的框架。更准确地说，后面的研究把 Agent 放进了更长、更真实的任务链条。\nCoT：先把“答案”拆成过程 对于多步计算或符号推理，直接要求最终答案，系统无法暴露中间哪一步出了问题。Chain-of-Thought Prompting 的做法是提供“问题—中间推理—答案”的示例，让模型生成中间步骤再到达结论。\n论文报告，在 GSM8K 等任务上，540B 参数的 PaLM 配合 8 个 CoT 示例取得了当时的领先表现。这是论文在特定模型与 few-shot 设置下的结果，不是所有模型都会自动获得的保证。Chain-of-Thought Prompting\nCoT 解决的是“如何展开思考”。它没有让模型查询数据库、读取网页或运行程序；一条流畅的推理链仍可能建立在错误前提上。\n关键洞察：CoT 让答案过程可见，但还没有让推理接受环境校验。\nReAct：把环境反馈送回下一轮推理 当问题依赖外部事实或真实操作时，人们自然会想：模型能不能先判断缺什么，再行动，再根据结果决定下一步？\nReAct 将 reasoning traces 和 actions 交织：模型输出 Thought 与 Action，运行时执行 Action 并返回 Observation，Observation 再成为下一轮 Thought 的输入。ReAct\n论文在问答、事实验证及交互任务上评估该范式；在只给 1～2 个 in-context 示例的设置中，论文报告 ReAct 在 ALFWorld 与 WebShop 上相对相关基线分别有 34 与 10 个百分点的绝对提升。该数字只描述论文的对应实验，不能外推为任意 Agent 的成功率。ReAct\n这也是工程里常见的 Agent loop：LLM → Action → Tool → Observation → LLM。重点不是把输出格式命名为 Thought，而是 Observation 必须来自可执行的外部环境。\n关键洞察：CoT 让模型解释自己的思路；ReAct 让思路能够被现实结果修正。\nToolformer：把“能调工具”与“会用工具”分开 ReAct 描述了推理与行动如何交替，但仍有一层问题：模型如何判断要不要调用工具、选择哪一个 API、填哪些参数，以及如何使用返回值？\nToolformer 研究的正是这种自监督的工具使用学习：模型学习 API 的调用时机、参数与结果整合。Toolformer\n这与今天工程里常说的 Function Calling 或 MCP 不能混为一谈：\n层次 关注点 ReAct 推理、行动、观察如何组成一个运行循环 Toolformer 模型如何学习 API 使用行为 Function Calling API 应用怎样接收结构化调用请求 MCP Agent 客户端怎样与工具提供方建立标准连接 所以，tool_calls JSON 解决的是接口表达；它不等同于模型已经具备可靠的工具选择能力。一个工程系统仍要负责执行、校验参数、限制权限，并把结果回填到上下文。\n关键洞察：协议让工具“可被调用”，学习与运行时设计才决定工具“会不会被正确调用”。\nReflexion 与 Voyager：经验要么成为反馈，要么成为资产 一个 ReAct 轨迹结束后，下一次类似任务能否少走弯路？Reflexion 的答案是把外部反馈转写为语言反思，保存为 episodic memory，在后续尝试时作为上下文使用；它并不要求更新模型权重。Reflexion\n论文在 HumanEval 的特定设置中报告 91% pass@1，并以其中的 GPT-4 对照结果 80% 作比较。这个数字属于论文实验条件，不能当作当今模型或任意实现的通用结论。Reflexion\nVoyager 再往前走一步：在 Minecraft 中结合自动课程、不断增长的可执行 Skill Library，以及环境反馈、执行错误与自我验证的迭代提示，让完成过的行为可被再次调用。Voyager\nReflexion 主要留下“下次应该注意什么”的语言反馈；Voyager 的 Skill Library 试图留下“已经会做什么”的可执行资产。两者都不是企业平台的现成蓝图：前者需要控制反思的质量与检索，后者的 Minecraft 结果也不能直接等同于业务系统。但它们明确了经验积累的两种工程形态。\n关键洞察：反馈能减少重复犯错；被验证、可调用的行为才可能成为 Skill。\nSWE-agent：能力还取决于 Agent 看见怎样的界面 把 Agent 放进代码仓库，任务不再只是回答：它要逐步浏览文件、搜索符号、局部修改、运行测试、读取报错，再决定是否继续。此时“给一个万能 shell”并不必然带来稳定的任务推进。\nSWE-agent 提出 Agent-Computer Interface（ACI），讨论为仓库浏览、编辑与测试等活动设计更适合 Agent 的交互界面。论文在其评测设置中报告 SWE-bench 12.5% 和 HumanEvalFix 87.7% 的 pass@1；这些数值同样受模型、提示、接口和评测版本约束。SWE-agent\n关注点 不利于 Agent 的接口 面向 Agent 的接口目标 浏览代码 一次返回大量无关内容 支持逐步定位与搜索 修改文件 整文件覆盖、难以审查 局部且可验证的编辑 执行测试 只有成功或失败 返回可定位、可继续推理的反馈 权限边界 任意命令直接执行 明确能力范围与审计点 这把讨论从“模型是否够聪明”推进到“环境是否给了恰当的可观察性与可操作性”。对于 Coding Agent，工具颗粒度、输出格式、沙箱和测试反馈都是能力的一部分。\n关键洞察：Agent 的性能不只来自模型，也来自它能够安全、清楚地感知和操作的环境。\n动手：跑一遍可验证的 ReAct 控制流 配套 demo 位于 GYA/c10-agent-theory-timeline。默认是离线 replay，Python 3.9+、无第三方依赖、无 API Key；它验证控制流，不模拟论文 benchmark，也不声称调用真实模型。\ngit clone https://github.com/renxin2024/GYA.git cd GYA/c10-agent-theory-timeline python3 main.py 本次以 python3 --version \u0026amp;\u0026amp; python3 main.py 实际运行，输出如下：\nPython 3.9.6 [Thought] I need an external fact before answering. [Action] lookup({\u0026#34;query\u0026#34;: \u0026#34;capital of France\u0026#34;}) [Observation] Paris is the capital and most populous city of France. [Final Answer] The answer is Paris is the capital and most populous city of France. [check] actions=1 observations=1 terminated=final_answer 跑通标准不是记住“巴黎”，而是看到一次 Action → Observation → Final Answer 的闭环。若执行时出现语法错误，请确认 python3 --version 不低于 3.9；若使用 --live 报 DEEPSEEK_API_KEY is required，这是正常的运行时保护，需配置密钥以及兼容的 LLM_API_URL、LLM_MODEL。本文未运行 --live，因此不报告其输出或效果。\n下一步：按能力缺口设计，而不是按论文堆组件 读这条脉络时，最有用的问题不是“下一篇 Agent 论文叫什么”，而是：当前任务到底缺少推理展开、外部观察、工具选择、失败反馈、可复用 Skill，还是环境接口？\n这个判断决定你该改 prompt、工具契约、记忆策略、Skill 流程还是执行环境。知识检索与 RAG 是另一条专题路线，本文不展开。\n下一篇将从 Agent 运行轨迹出发，讨论怎样把反复出现、且已经验证过的做法沉淀为新 Skill。\n理解检验 为什么 CoT 的中间步骤不能替代 ReAct 的 Observation？ 为什么 Function Calling 的 JSON 契约不等于 Toolformer 的研究问题？ Reflexion 的语言反馈与 Voyager 的可执行 Skill 分别留下了什么？ 面对一个不稳定的 Coding Agent，为什么先审视 ACI 可能比先换模型更有效？ 参考资料 Chain-of-Thought Prompting Elicits Reasoning in Large Language Models ReAct: Synergizing Reasoning and Acting in Language Models Toolformer: Language Models Can Teach Themselves to Use Tools Reflexion: Language Agents with Verbal Reinforcement Learning Voyager: An Open-Ended Embodied Agent with Large Language Models SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering ","date":"2026-06-29T00:00:00Z","image":"/images/c10-agent-theory-cover.png","permalink":"/post/c10-agent-theory/","title":"第十话｜Agent 理论发展脉络：从 CoT 到 SWE-Agent"},{"content":"第八话里，工具接入终于有了统一的边界。\nAgent 想查天气，可以调用 MCP Server；想读数据库，也可以通过同一套协议发现和调用工具。工具来自另一个进程、另一种语言，甚至另一个团队时，调用方不必再为每一项能力重写适配代码。\n可一个任务通常不只缺一个工具。\n让 Agent 检查一篇文章，它也许找得到文件检查工具，却不知道应先看 frontmatter 还是标题；它也许发现了错误，却不知道能否直接改原文；它也许完成了几步操作，却没有明确的完成判定。工具越来越多，“应该怎样把事情做完整”反而更值得被固定下来。\n先说结论：\nMCP 解决“Agent 能调用什么”，Skill 解决“Agent 应该按什么方法完成任务”。\nSkill 不是把 Prompt 写得更长。它把一套经过思考的方法拆成能被发现、按需加载、执行和验证的资源。\n一、工具暴露动作，Skill 组织方法 假设我们有一个 Markdown 检查工具。它能接收文件路径、返回检查结果，也许还支持修复参数。这些信息足以描述一个动作，却不能自动给出一条可靠的任务路径。\n任务问题 单个工具通常能否回答 检查哪个文件 可以 先检查 frontmatter 还是标题 不一定 失败后是否继续 不一定 能否直接修改原文 不能替代权限约束 什么结果算任务完成 需要额外规则 这和 Java 服务里已经注册了一组接口很像：checkFrontmatter()、checkHeading()、checkLinks() 都可调用，并不等于“文章质量检查”这个业务流程已经存在。顺序、分支、验收标准和修改权限仍需要被组织起来。\n关键洞察：工具把能力暴露出来，Skill 把能力组织成方法。\n二、一个 Skill 到底保存了什么 人说“我会检查博客文章”时，脑中通常不只是一句提醒，还包括适用场景、检查步骤、参考标准、可以交给脚本的部分，以及失败时应如何收尾。\nAgent Skills Specification 规定，一个 Skill 至少是包含 SKILL.md 的目录；scripts/、references/ 和 assets/ 都是按任务需要才加入的可选资源。真正重要的不是目录长相，而是把原本藏在人脑中的做法，拆成可审阅、可替换的职责。\n图中有一个容易被忽略的顺序：先用 name + description 判断这个 Skill 是否相关；匹配后才加载 SKILL.md；执行时再按需要读取参考资料、调用脚本或复用资产。目录的作用不是堆文件，而是让不同类型的知识在恰当的时机进入任务。\n一个过于抽象的写法往往没有可执行性：\n# Markdown 质量检查 请认真、全面地检查文章。 下面这种写法才把“做事方法”落到了可操作的层面：\n--- name: markdown-quality description: Check Markdown articles for required frontmatter, one H1 title, and a closing summary. --- 1. 读取 `references/checklist.md`。 2. 运行 `scripts/validate.py \u0026lt;markdown-file\u0026gt;`。 3. 报告每条失败规则和可操作的修复建议。 4. 未经明确请求，不修改文章。 差别不在于后者更长，而在于它明确了输入、步骤、确定性工具、输出和权限边界。\n三、Skill 如何被发现，又为何不必一次读完 一个 Agent 不需要在每次任务开始时，把所有 Skill 的全部说明、资料和脚本都塞进上下文。官方 Quickstart 把过程拆成发现、激活和执行：启动时先看到技能的 name 与 description；任务匹配后再读取完整 SKILL.md；真正执行时才访问需要的资源。Agent Skills Quickstart\n这张图描述的是分层加载，而不是某个模型保证会做出的路由决定。description 写得太泛，匹配阶段就缺少足够线索；把无关资料全塞进入口文件，又会让每次执行背负不必要的上下文。一个好的描述更像服务注册中心里的服务名片：它不执行业务，却决定调用方能否先找到正确入口。\n关键洞察：Skill 的入口要足够具体以便被发现，细节则应在真正需要时才加载。\n四、动手：运行一个最小 Skill Host 这一节不模拟 LLM 推理，也不声称模型一定会选择正确 Skill。它只把一个 Skill Host 的协议边界固定下来：扫描 frontmatter、按 description 做最小匹配、加载指令、调用确定性验证脚本。\n代码位于 GYA 的 c09-skill-system：\nc09-skill-system/ ├── main.py ├── skills/ │ └── markdown-quality/ │ ├── SKILL.md │ ├── references/checklist.md │ └── scripts/validate.py └── samples/ ├── good.md └── bad.md 运行环境只有 Python 3.9+，不依赖第三方包，也不需要 API Key：\ngit clone https://github.com/renxin2024/GYA.git cd GYA/c09-skill-system python3 main.py 2026-08-22 在 Python 3.9.6 上的实际输出如下：\n[discover] markdown-quality [match] markdown-quality [load] SKILL.md + references/checklist.md [validate] good.md -\u0026gt; PASS: frontmatter=ok h1=1 summary=ok [validate] bad.md -\u0026gt; FAIL: frontmatter must be delimited by ---; missing closing section: ## 总结 [validate] passed=1 failed=1 (expected) [result] status=PASS 这里最后的 status=PASS 不表示两份样例都合格。它表示 Host 的验收条件被满足：合格样例通过，故意损坏的样例被拦截。把“验证器工作正常”和“被验证对象合格”分开，是写 Skill 时很重要的工程习惯。\n如果运行结果不同，先按下面顺序排查：\n确认当前目录是 GYA/c09-skill-system，否则 main.py 找不到相对路径下的 Skill 和样例。 确认使用 Python 3.9+；本 demo 使用了 Python 3.9 开始支持的内置泛型类型标注。 检查 skills/markdown-quality/SKILL.md 是否保留了 references/checklist.md 这一引用；demo 会把缺少该引用视为无效 Skill。 五、哪些工作交给模型，哪些交给脚本 Skill 不等于“把所有步骤都交给脚本”。模型仍然适合开放判断，例如观点是否清楚、语气是否自然、解释是否遗漏读者前提。脚本更适合结果只有明确对错的事情。\n工作 更适合的职责方 原因 判断文章观点是否清楚 模型 需要语义理解与上下文判断 判断语气是否自然 模型 需要综合语言和读者语境 统计 H1 数量 脚本 可得到稳定的确定性结果 检查 JSON 能否解析 脚本 成功或失败有明确判定 检查文件是否存在 脚本 不需要模型猜测 删除文件、发送消息、部署 权限系统与人工审批 正确性不等于被授权 模型处理语义，脚本锁住不应漂移的事实，权限系统控制不可逆影响。Skill 的价值正是在任务层把这三类职责串成一条可检查的方法，而不是把其中任何一类神化成万能方案。\n六、Prompt、Memory、MCP、Skill 各自解决什么 这几个词经常同时出现在 Agent 架构里，但它们回答的问题不同。\n能力 保存或提供什么 主要回答的问题 不能替代什么 Prompt 当前任务的即时指令与上下文 “这次希望我怎样回答？” 可复用工作流与长期事实 Memory 过去的事实、偏好或经验 “上次我们决定了什么？” 工具接入和任务方法 MCP 可发现、可调用的工具与资源 “现在能做什么？” 步骤、验收和权限策略 Skill 完成一类任务的方法与边界 “这件事应该怎样完成？” 模型推理、外部工具本身和授权 在一个真实任务里，Memory 可以补充用户偏好，MCP 可以提供读写外部系统的工具，Skill 可以规定检查流程，Agent Loop 负责推进步骤，脚本负责验证局部结果。它们相互协作，但不应互相冒充。\n七、从 demo 到生产，Skill 还要补什么 把 SKILL.md 写出来只是起点。一个错误的 Skill 可能让 Agent 更稳定地执行错误流程，所以生产环境至少还要补上几件事：\n用真实样例检验它是否会在该触发时被发现； 用故意失败的样例确认验证器真的能拦住错误； 为规则变化保留版本和原因； 将写文件、发消息、发布、部署等高影响动作放在明确的权限边界后； 记录执行轨迹，把重复失败沉淀为下一版规则。 这些是从本文 demo 推导出的工程实践，不代表这个几十行的示例已经具备完整生产能力。它的职责只有一个：让“发现—加载—执行—验证”的边界变得可见、可运行、可讨论。\n结尾：方法也应成为可复用的能力 第八话解决了工具如何标准化接入；第九话继续回答工具接入之后的事：怎样把经过验证的做法保存下来，让下一次任务不必从零猜测。\n当 Agent 既能调用工具，又能复用方法时，新的追问自然出现：这些推理和行动方式又是怎样一步步演化出来的？下一篇，我们沿着 CoT、ReAct、Toolformer、Reflexion、Voyager 到 SWE-Agent 的线索继续往前看。\n理解检验 如果一个 Skill 只有 SKILL.md，没有脚本和参考资料，它仍然可以成立吗？为什么？ 为什么“验证器通过”和“被验证对象通过”不能混为一谈？ 当一个任务需要读取用户偏好、调用文件工具、并检查输出格式时，Memory、MCP、Skill 和脚本各自承担什么职责？ 参考资料 Agent Skills Specification：Skill 的最小目录、frontmatter、可选资源与渐进加载约定。 Agent Skills Quickstart：发现、激活和执行的最小示例。 GYA 第九话 demo：本文配套的最小 Skill Host；本文运行记录验证日期为 2026-08-22。 ","date":"2026-06-19T00:00:00Z","image":"/images/c09-skill-system-cover.png","permalink":"/post/c09-skill-system/","title":"第九话｜Skill 系统：把做法变成资产"},{"content":" 第四话里，我们把工具放进了一个 ToolRegistry。工具多了以后，注册表比 if-else 强很多：可以发现工具、统一调用、返回结构化错误。\n但它有一个隐含前提：工具和 Agent 在同一个进程，至少在同一套代码里。\n如果天气服务是另一个 Python 进程，数据库工具是 Java 服务，GitHub 工具由另一个团队维护，第四话的注册表还能直接解决吗？不能。你需要给每一种外部工具写一套适配器，工具数量和接入方数量一增加，就会重新掉进 N×M 集成地狱。\n这就是 MCP 要解决的问题。\nToolRegistry 管理进程内工具，MCP 标准化工具提供方和 Agent 客户端之间的边界。\n一、旧世界：工具被绑在调用方代码里 第四话的最小闭环是：模型输出工具名和参数，注册表根据名称找到函数，函数执行后返回 ToolResponse。\nLLM → ToolRegistry → Python 函数 → ToolResponse → LLM 这个结构对学习和单体应用非常好。你能清楚看到每一层的职责，也能自己控制错误码和参数校验。\n问题出在边界。假设 Agent 需要 20 个工具，而这 20 个工具分布在 5 个独立服务里。没有统一协议时，客户端要知道每个服务如何启动、如何描述工具、如何传参数、如何返回结果。每接一个服务，都要重新写一套通信和适配代码。\nflowchart LR A[Agent 客户端] --\u003e P[多套私有适配器] P --\u003e S[多个外部服务]这不是工具本身太多，而是每个客户端都在重复理解每个服务的私有接口。\n二、MCP 把什么放到了协议层 MCP 可以先用三个角色理解：\nHost：承载 Agent 的应用，例如桌面助手、IDE 或你的 Python 程序； MCP Client：Host 内部的协议客户端，负责连接某一个 MCP Server； MCP Server：工具、资源或提示模板的提供方。 它们的关系不是“模型直接连接 MCP Server”，而是：\nHost └── MCP Client ── JSON-RPC/Transport ── MCP Server └── tools 一次最小工具调用大致经历：\nClient 与 Server 初始化并协商协议版本和能力； Client 请求工具列表； Server 返回工具名称、描述和输入 Schema； Client 发起工具调用； Server 执行函数并返回内容或错误。 这里有一个重要边界：MCP 标准化的是“怎么发现和调用”，不是“下一步该调用什么”。任务规划仍然由 Agent 循环和模型负责，工具的权限、业务正确性和结果验证仍然由应用负责。\n三、STDIO：先把跨进程边界跑通 MCP 支持多种传输方式。第八话先选择 STDIO，因为它最容易看清进程边界：Client 启动 Server 子进程，JSON-RPC 消息通过 stdin/stdout 传递。\nflowchart LR C[MCP Client] --\u003e I[initialize] I --\u003e L[tools/list] L --\u003e X[tools/call add] X --\u003e R[CallToolResult]STDIO 有一个很容易踩的坑：stdout 是协议通道，不是日志通道。 Server 如果把调试日志打印到 stdout，Client 读到的就不再是合法的 JSON-RPC 消息。日志应该写 stderr，或者通过 MCP 的 logging 能力发送。\n生产环境更常用 Streamable HTTP：Server 可以独立部署，多个 Client 通过网络连接。但这同时带来认证、超时、并发、TLS、会话和部署问题。先跑通 STDIO，再讨论 HTTP，学习成本更低。\n四、Python：用官方 SDK 跑通 Client/Server 前置环境 Python 3.10+；本次用 Python 3.11.15 实测； mcp[cli]==2.0.0； 不需要 LLM API Key。 代码位于 GYA/c08-mcp/。Server 只有一个工具：\nfrom mcp.server import MCPServer mcp = MCPServer(\u0026#34;c08-demo\u0026#34;) @mcp.tool() def add(a: int, b: int) -\u0026gt; int: \u0026#34;\u0026#34;\u0026#34;Add two integers and return the result.\u0026#34;\u0026#34;\u0026#34; return a + b if __name__ == \u0026#34;__main__\u0026#34;: mcp.run() 类型注解不仅服务于 Python 类型检查，也帮助 SDK 生成工具输入 Schema。Client 不需要自己手写 a、b 的 JSON Schema，就能通过 tools/list 发现它。\nasync with stdio_client(server) as (read_stream, write_stream): async with ClientSession(read_stream, write_stream) as client: await client.initialize() listed = await client.list_tools() result = await client.call_tool(\u0026#34;add\u0026#34;, {\u0026#34;a\u0026#34;: 2, \u0026#34;b\u0026#34;: 3}) 这里使用的是 mcp==2.0.0 的实际 API：stdio_client 返回读写流，再交给 ClientSession。在线文档中的更高层 Client(StdioServerParameters(...)) 写法属于更新的文档线，不能未经验证地复制到锁定版本。\n运行：\ncd GYA/c08-mcp python3 -m venv .venv source .venv/bin/activate python3 -m pip install -r requirements.txt python3 main.py 本次实测关键输出：\n[1] 初始化 MCP Server: experimental={} ... tools=ToolsCapability(list_changed=False) ... [2] 发现工具: [\u0026#39;add\u0026#39;] [3] 调用 add(2, 3): 5 [4] 错误场景: is_error=True; Unknown tool: missing_tool Python SDK 把未知工具作为一个带 is_error=True 的工具结果返回，Client 不会因为这个工具级错误直接崩溃。\n五、Java 21：同一个协议，不同的 SDK 表达 前置环境 Java 21； Gradle Wrapper 8.14.2； io.modelcontextprotocol.sdk:mcp:2.0.0； 不需要 LLM API Key。 代码位于 GYA-Java/c08-mcp/。Java 版使用 StdioServerTransportProvider 暴露 Server，使用 StdioClientTransport 启动 Server 子进程。工具定义需要显式提供输入 Schema：\nMcpSyncServer server = McpServer.sync(transport) .serverInfo(\u0026#34;c08-java-demo\u0026#34;, \u0026#34;1.0.0\u0026#34;) .capabilities(ServerCapabilities.builder().tools(true).build()) .toolCall( Tool.builder(\u0026#34;add\u0026#34;, schema) .description(\u0026#34;Add two integers\u0026#34;) .build(), (exchange, request) -\u0026gt; { int a = ((Number) request.arguments().get(\u0026#34;a\u0026#34;)).intValue(); int b = ((Number) request.arguments().get(\u0026#34;b\u0026#34;)).intValue(); return CallToolResult.builder() .content(List.of(new McpSchema.TextContent(Integer.toString(a + b)))) .build(); }) .build(); 运行：\ncd GYA-Java JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew :c08-mcp:run 本次实测关键输出：\n[1] 初始化 MCP Server: c08-java-demo [2] 发现工具: [add] [3] 调用 add(2, 3): 5 [4] 错误场景: McpError; Unknown tool: invalid_tool_name Java SDK 2.0.0 对未知工具的处理和 Python 不一样：它返回 JSON-RPC 错误，客户端表现为 McpError。这不是“谁对谁错”，而是两个 SDK 对协议级错误的 API 映射不同。跨语言教程不能只比较正常输出，也要把错误语义写清楚。\n六、MCP 和第四话 ToolRegistry 的边界 维度 第四话 ToolRegistry 第八话 MCP 工具位置 同一进程或代码库 独立进程或远程服务 发现方式 读取本地注册表 协议请求发现 调用边界 函数调用 JSON-RPC + 传输协议 多语言 需要自己适配 Client/Server 可以异构 主要价值 进程内工具管理 工具接入与复用 没有解决 任务规划、权限、结果验证 任务规划、权限、结果验证 MCP 不是 Agent 的替代品，也不是把任何函数自动变成可靠工具的魔法。它只是把原来散落在每个应用里的“如何连接工具”这部分共性抽出来，变成一套可复用的协议。\n七、从 Demo 到生产还缺什么 这个 demo 故意没有 API Key，也没有真实业务副作用。生产 MCP Server 至少还需要：\n输入 Schema 和业务层双重校验； 超时、重试和幂等策略； 认证、授权和最小权限； 不把凭据通过环境继承或日志泄露； 对工具结果做真实性和完整性验证； 对 Server、Client、工具调用和错误建立可观测性。 尤其要记住：MCP 的“能调用”不等于业务上的“允许调用”。 文件删除、支付、发邮件、修改生产数据，都需要在协议之外增加权限和人工审批边界。\n下一篇：Skill 把“做法”变成资产 现在我们解决了“工具在哪里、怎么发现、怎么调用”。但还有一个问题：工具能调用，不代表 Agent 知道应该如何完成一个复杂任务。\n下一篇进入 Skill：把触发条件、步骤、约束、脚本和验证方式封装成可复用能力。工具解决“能做什么”，Skill 解决“应该怎么做”。\n参考资料与实测记录 Model Context Protocol 官方 Python SDK（Python 3.11.15、mcp[cli]==2.0.0，本轮验证记录：2026-08-21） MCP Python SDK Client Transports（STDIO 传输说明，本轮核对：2026-08-21） Model Context Protocol 官方 Java SDK（Java 21、SDK 2.0.0，本轮验证记录：2026-08-21） MCP Java SDK Client（STDIO Client 与工具调用 API，本轮核对：2026-08-21） MCP Java SDK Server（STDIO Server 与 Tool Specification，本轮核对：2026-08-21） ","date":"2026-06-09T00:00:00Z","image":"/images/c08-mcp-cover.png","permalink":"/post/c08-mcp/","title":"第八话｜MCP 协议：工具为什么需要一根 USB-C"},{"content":" 第六话里，我们把\u0026quot;流程控制\u0026quot;从模型手里收回来，交给了 StateGraph。但有一个问题从第五话开始就一直存在，我们假装它不存在：\n模型的记忆。\n回顾第五话的代码，我们的\u0026quot;记忆\u0026quot;就是那个 messages 列表——每次对话，把所有历史都塞进 prompt 喂给模型：\nstate.add_message(msg) # 工具调用也塞进历史 state.add_message({\u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34;, ...}) # 观察结果也塞进历史 前几轮没事。但对话到 20 轮、30 轮呢？prompt 越来越长，直到撞上上下文窗口上限——这时会发生什么？模型开始\u0026quot;忘记\u0026quot;最早说的话，或者更糟：被中间的信息干扰，答错。\n我先说结论：\n上下文窗口 ≠ 记忆。 模型本身不\u0026quot;记得\u0026quot;任何东西，它只是每次被我们喂一坨 prompt。 真正的记忆分三层：\n上下文（Working）：当前窗口内，最易失——塞不下就丢 短期（Episodic）：会话内关键事实，显式提取——比全量历史省 token、抗干扰 长期（Semantic）：跨会话持久，检索式访问——不检索的记忆等于不存在 这一篇，我们写一个带三层记忆的最小 Agent，亲眼看看\u0026quot;记忆是怎么补位的\u0026quot;。\n一、旧世界：把历史塞进上下文的问题 先看最朴素的做法——全量历史注入。它有两个硬伤：\n硬伤 1：上下文窗口有限。 假设窗口 8K token，对话到第 30 轮时历史已经 10K token——塞不进去了。要么截断最老的（等于\u0026quot;失忆\u0026quot;），要么压缩（损失细节）。窗口是物理限制，记忆是软件设计。\n硬伤 2：Lost in the Middle（中间迷失）。 2023 年一篇论文（arXiv 2309.04271）发现一个反直觉现象：模型对长上下文的利用呈 U 型——开头和结尾的信息记得好，中间的信息最容易丢，性能差距可达 25-30%，即使 100K 窗口的模型也一样。把关键信息放在 30 条消息的中间？它很可能被\u0026quot;淹没\u0026quot;。\nflowchart LR A[对话历史\n不断增长] --\u003e B{超过窗口?} B --\u003e|是| C[截断最老\n= 失忆] B --\u003e|否| D[全量塞入\n中间信息易丢] 关键洞察：把历史当\u0026quot;水管\u0026quot;一样无脑灌，是新手 Agent 最大的坑。塞不下和塞中间，都是\u0026quot;假记忆\u0026quot;——看似有，实则不可靠。\n二、转折：三层记忆——各管一段 认知科学早就给了我们答案：人的记忆也不是一个池子，而是分层的。Agent 照搬这个分层：\n层 存什么 生命周期 访问方式 类比 上下文（Working） 当前轮次消息 窗口内 直接注入 prompt 你正在读的这句话 短期（Episodic） 会话内关键事实 会话级 显式提取 + 摘要注入 今天上午聊过的事 长期（Semantic） 用户偏好/知识点 跨会话（近乎永久） 向量检索 你记得朋友爱喝什么 关键设计原则：\n① 短期记忆 = 显式提取，不是全量存储。 全量历史是\u0026quot;录像带\u0026quot;——什么都录了，但长、贵、噪音多。短期记忆是\u0026quot;笔记\u0026quot;——只记关键事实（名字、偏好、决定），结构化、省 token、抗干扰。\n② 长期记忆 = 检索式访问，不是注入式。 你不可能把\u0026quot;用户爱喝龙井、职业是 Java 工程师、博客写 AI Agent\u0026quot;全塞进每次 prompt。正确做法是：提问时检索出相关的几条，只注入那几条。这就是 RAG（检索增强生成）的核心思想——记忆库很大，但每次只\u0026quot;想\u0026quot;起相关的部分。\n③ 记忆要有生命周期：巩固与遗忘。 不是所有信息都值得长期保存。工程实践通常给记忆打分（importance）：高价值 → 升级到长期；低价值 → 过期遗忘。遗忘不是 bug，是 feature——没有遗忘，记忆库会堆满垃圾，检索质量反而下降。\nflowchart LR A[对话] --\u003e B{短期记忆\n提取关键事实} B --\u003e C[上下文\n当前轮] B --\u003e D[长期记忆\n向量检索] C --\u003e E[回答] D --\u003e E 关键洞察：三层记忆不是\u0026quot;三个数据库\u0026quot;，而是三种访问模式——上下文是注入式（全量）、短期是摘要式（精炼）、长期是检索式（按需）。访问模式决定了它们各自解决什么问题。\n三、原理讲透：为什么\u0026quot;不检索的记忆等于不存在\u0026quot; 很多人以为\u0026quot;我的 Agent 存了聊天记录，所以它有记忆\u0026quot;。错。存储 ≠ 记忆，检索才是记忆。\n想想人的记忆：你知道\u0026quot;朋友爱喝龙井\u0026quot;这件事，不是因为你把这句话背下来了，而是因为在需要的时候你能想起来——聊天聊到茶、点单、送礼，这些场景会自动唤起这条记忆。\nAgent 也一样。长期记忆必须支持按语义检索：用户问\u0026quot;他喜欢喝什么？\u0026quot;，Agent 要从记忆库里找出\u0026ldquo;喜欢喝茶，尤其是龙井\u0026quot;这条。怎么找？最基础的方法是向量相似度：\n1. 把记忆条目转成向量（embedding）——语义相近的文本向量也相近 2. 把用户问题也转成向量 3. 计算余弦相似度，返回最相近的 N 条 这就是\u0026quot;检索\u0026quot;的本质。演示里我们用纯 Python 实现一个最小版（中文 bigram 分词 + 余弦相似度，零依赖），让你看到机制本身：\ndef tokenize(text): # 中文按相邻两字（bigram）切分 return {text[i:i+2] for i in range(len(text)-1) if 中文字符} def cosine(a, b): # 向量相似度 return len(a \u0026amp; b) / (sqrt(len(a)) * sqrt(len(b))) 注意演示级实现的局限：纯 bigram 对同义改写敏感——\u0026ldquo;爱喝\u0026quot;和\u0026quot;喜欢喝\u0026quot;切出来的 bigram 完全不同，匹配不到。生产环境用真实 embedding（如 OpenAI/DeepSeek embedding API），同义词会投影到相近的向量空间，就没有这个问题。机制相同，精度不同。\n顺带说一句检索之外的另一半：巩固（Consolidation）与遗忘（Forgetting）。记忆不是存进去就完事了——它有一个生命周期：\n对话产生信息 → 编码 → 存储 → 检索 → 巩固（低→高价值迁移）→ 遗忘（过期清理） 工程上最常见的做法是给每条记忆打 importance 分数：\n巩固：对话中出现的用户偏好/重要决定，打分 ≥ 0.7 → 从短期升级到长期；分数低的留在短期，会话结束即丢。这模拟了人脑\u0026quot;重要的事记牢，琐事随风散\u0026rdquo;。 遗忘：长期记忆也有容量和时效。时间衰减（太久没被检索到的降权）、容量上限（满了淘汰最不重要的）。没有遗忘的记忆库，最终会被垃圾淹没，检索质量断崖式下降——这跟搜索引擎要处理\u0026quot;死链\u0026quot;是一个道理。 很多 Agent 项目死在\u0026quot;只存不捡\u0026rdquo;：记忆越堆越多，检索命中率越来越差。好的记忆系统不是存得多，而是记得准、忘得掉。\n四、动手：三层记忆 Agent 实测 代码：https://github.com/renxin2024/GYA/tree/main/c07-memory（memory_demo.py，纯标准库）\nexport DEEPSEEK_API_KEY=sk-你的key python3 memory_demo.py 场景 1：短期记忆补位 用户第一轮说\u0026quot;我叫张三\u0026quot; → Agent 提取存入短期记忆。然后聊两轮无关话题（写文字）——此时张三的名字已经不在模型上下文里了。再问\u0026quot;我是谁？我叫什么名字？\u0026quot;：\n模型(带记忆): 你是张三。 对比（无记忆，没有任何上下文）: 模型(无记忆): 在没有上下文的情况下，我无法知道你是谁，也不知道你的名字。 这就是短期记忆的价值：名字被\u0026quot;挤出\u0026quot;上下文后，显式提取的记忆补上了。注意我们没有把整个对话历史塞回去——只注入了一条关键事实，更省 token、更抗干扰。\n场景 2：长期记忆向量检索 向记忆库存入三条用户画像，然后提问检索：\n记忆库: - 用户喜欢喝茶，尤其是龙井 - 用户职业是 Java 后端工程师，擅长并发编程 - 用户的博客主题是 AI Agent 开发 问『用户喜欢喝什么？』→ 检索到: 用户喜欢喝茶，尤其是龙井 问『用户职业是什么？』→ 检索到: 用户职业是 Java 后端工程师... 问『博客写什么？』→ 检索到: 用户的博客主题是 AI Agent 开发 每条问题都准确\u0026quot;想\u0026quot;起了对应的记忆——这就是检索式访问。如果记忆库有 1000 条，我们不会全塞给模型，只注入检索到的那 1-2 条。\n场景 3（诚实说明）：Lost-in-the-Middle 实测 演示脚本里也放了一个\u0026quot;信息放中间 vs 开头\u0026quot;的对比。诚实地说：deepseek-v4-flash 对短 padding 抗性很好，两头都答对了——这个效应要在 100K 级长上下文才明显（论文数据：中间 vs 两端性能差 25-30%）。demo 放它是让你知道这个坑的存在，正文用论文数据支撑结论。\nflowchart LR A[用户提问] --\u003e B{检索长期记忆} B --\u003e C[命中 1-2 条] C --\u003e D[注入上下文] D --\u003e E[模型回答] A --\u003e F[短期记忆摘要] F --\u003e D 关键洞察：三层记忆合起来的完整流程是——提问时，短期记忆给摘要、长期记忆给检索结果，一起注入上下文，模型基于\u0026quot;当前问题 + 记得的事\u0026quot;作答。模型始终只面对一个\u0026quot;够用的小 prompt\u0026quot;，而不是越来越长的全量历史。\n五、工程化延伸：生产级记忆系统 demo 是概念演示，生产要补的东西不少：\n能力 demo（本篇） 生产要补的 短期记忆 内存 dict 会话级存储 + 上限管理（FIFO/TTL） 长期记忆 内存 list + bigram 向量库（Qdrant/Chroma）+ 真实 embedding 巩固 无 重要性打分（如 ≥0.7 升长期）+ 定期 Consolidation 遗忘 无 时间衰减/容量上限，防止记忆库膨胀 记忆 vs RAG 混在一起 明确边界：Memory 存对话产生的个人事实，RAG 存外部知识库 记忆 vs RAG 的边界值得单独强调——这是最常见的混淆：\nMemory（记忆）：对话中自然产生的——\u0026ldquo;用户叫张三\u0026quot;\u0026ldquo;用户爱喝龙井\u0026rdquo;。换个用户，答案就变。 RAG（检索增强）：外部知识库——\u0026ldquo;什么是 ReAct\u0026quot;\u0026ldquo;Java 并发怎么调优\u0026rdquo;。所有用户共享，答案不变。 把用户偏好存进 RAG？你会得到\u0026quot;所有用户都爱喝龙井\u0026rdquo;。把外部知识存进 Memory？每次都要重新\u0026quot;对话产生\u0026rdquo;，浪费。各司其职。\n下一步 阶段 2（Agent 循环）到此收尾：我们有循环（第五话）、有状态（第六话）、有记忆（第七话）——一个\u0026quot;会干活、能记住\u0026quot;的 Agent 成型了。\n但还有一个痛苦没解决：工具的接入。第四话我们手写了注册表，接一个新工具要写代码、注册、测试。如果你要接 10 个不同的服务（GitHub、数据库、日历、邮件），每个都要写适配代码——N×M 的集成地狱。\n下一篇进入阶段 3：MCP 协议——为什么需要它。一句话预告：MCP 是\u0026quot;工具的 USB-C 接口\u0026quot;，让工具接入标准化。\n演示代码：https://github.com/renxin2024/GYA/tree/main/c07-memory（memory_demo.py，纯标准库；DeepSeek 官方 API，默认模型 deepseek-v4-flash）。Java 21 等价实现见独立仓库 https://github.com/renxin2024/GYA-Java（c07-memory/，Gradle 工程，gradle run）。 参考：Lost in the Middle 论文 arXiv 2309.04271（U 型注意力、中间性能差 25-30%）；四层记忆与巩固/遗忘机制来自 Agent 记忆系统工程实践（hello-agents 讲义延伸）；Memory vs RAG 边界为行业共识；demo 输出为 2026-05-26 本机实测（deepseek-v4-flash，Python 与 Java 双跑）。\n","date":"2026-05-26T00:00:00Z","image":"/images/c07-memory-cover.png","permalink":"/post/c07-memory/","title":"第七话｜记忆：上下文、短期、长期"},{"content":"第五话我们让模型的「想」和「做」交替起来了：模型想一步、做一步、看一眼真实结果、再想一步。循环跑通了。\n但那个循环藏着一个没解决的口子：它跑的是「一口气」——从头到尾，中间不能断。 一旦进程崩了、断电了、被 OOM 杀了，走到哪一步、做过什么事，全没了；硬要重跑，它又从第一步开始，把已经做过的事再从头做一遍。\n如果一个任务只是「查一次天气」，断了大不了重查，无所谓。但如果任务是「处理订单」——查库存 → 扣库存 → 发通知，三步里有两步是不可撤销的（扣了钱、发了通知），那「断了重来」就会出大事：扣库存会被重复执行，用户被扣两次钱。\n所以这一篇要讲的，就是怎么让循环不再是「只能一口气跑完」：断了能接回来，重跑时做过的事不会重做。\n而这件事的病根，其实只有一个——进度只活在内存里，没有任何可靠的、能查的痕迹。 抓住这个病根往下看，问题会一个个现形，解法也会一个个浮出来。这一篇就从「先看清这些问题」开始。\n一、没有状态管理的循环，问题在哪 第五话的循环，是靠 messages 这个列表撑起来的：模型说过什么、工具返回过什么，全都往里塞，靠它一路\u0026quot;喂\u0026quot;下去。这套东西跑一次性的任务没问题，但它没有「状态管理」——进度、做过的事，全都隐式地混在对话里、活在内存里。这带来几个实打实的毛病：\n看不清走到哪了。进度藏在对话里，你想知道\u0026quot;现在到第几步了、还差几步\u0026quot;，得把整段对话从头翻一遍，靠猜。没有一处能直接告诉你答案。\n断不起。所有数据都在内存里，进程一崩、一断电、一 OOM，全部归零。走到哪了、做过了什么，连个影子都不剩——只能从头再来。\n会重做。即便你硬着头皮重跑，它也不知道自己\u0026quot;已经做过哪些事了\u0026quot;。不可撤销的步骤（扣库存、发通知、退款）会被重复执行，用户被扣两次钱。\n这三条，是\u0026quot;没有状态管理\u0026quot;的循环共同的毛病。它们背后是同一个病根：进度没有被当成一等公民对待——它没被单独存下来、单独落盘、单独记录。 后面几节，就是顺着这三条，一条条把它们解决掉。\n二、提取状态，不和消息耦合 第一节说的第 1 条毛病（看不清走到哪），根子是进度和消息搅在一起。所以先从这儿下手——把「进度」从「对话」里拆出来。\n第五话把什么都塞进 messages，这一篇给它松绑：\n@dataclass class AgentState: task: dict # 任务输入（order_id / sku） messages: list # 只存「模型需要看到的对话」 trace: list # 执行轨迹（只读审计） status: RunStatus # 运行状态 next_step: str # 走到哪了（进度） done_steps: list # 已经做完了哪些步骤（防重复的依据） 多出来的两个字段，正好对应那几条毛病：\nnext_step：走到哪了。之前是「模型隐式决定下一步」，现在是「一个明确的字段指出来」。进度不再藏在对话里，一眼能看清——第 1 条毛病（看不清走到哪）就此解决。 done_steps：做过什么。它是后面「断了之后，哪些不用重做」的依据——为第 3 条毛病（会重做）铺路。 为什么这俩字段这么关键？因为**「看得清进度」和「不重复做」，都要求进度能被单独拎出来、一眼看清、能被存下来。** next_step 告诉你走到哪，done_steps 告诉你做过什么——有了这两样，后面落盘、恢复才有了抓手。\n三、保存磁盘的时机选择：看关键步骤 进度摆到明面了，但它还只在内存里，进程一崩照样归零。要让它「断了还在」，就得把它存到磁盘上：\nclass CheckpointStore: def save(self, state): json.dump(...) # 序列化落盘 def load(self): return ... # 读回一个完整的进度 「存」这个动作谁都会写（json.dumps 而已），真正关键的是什么时候存。这里有一个工程上反复验证过的判断：\n每个「有副作用」的步骤执行完，立即落盘。\n为什么是「有副作用」的步骤？因为无副作用的步骤（比如查一次库存），重做一百次结果都一样，断了大不了重跑。但有副作用的步骤（扣库存、发通知、退款），一旦执行了就不可撤销——必须在它执行完的那一刻，立刻把「我已经做过了」这个事实写到磁盘上。\nif call.name in {\u0026#34;deduct_inventory\u0026#34;, \u0026#34;notify_shipped\u0026#34;} and 成功: state.done_steps.append(这一步) checkpoint.save(state) # 立即落盘，绝不拖延 这一步，是「断了还能接着跑」的全部秘密。\n四、中断恢复时，防重复执行 进度落盘了，断点就能恢复。但恢复时最怕一件事：重放。\n还是那个订单：查库存 → 扣库存 → 发通知。进程在「扣库存」之后、「发通知」之前崩了。恢复时，模型从落盘的进度接着走，但它可能不知道自己「扣库存」已经做过了，于是又扣了一次——用户被扣了两次钱。\n为什么会这样？因为**「从 checkpoint 恢复」这件事，本质是「至少一次」（at-least-once）语义，不是「恰好一次」（exactly-once）。** 它保证的是「断了能接回来、不会漏做」，但保证不了「已经做过的绝不重做」——扣库存那个动作，可能在「已经扣了」和「落盘记录了」之间那几毫秒里崩掉，恢复时谁都不知道它到底做没做。\n所以「防重复」不能只靠 checkpoint，还得靠幂等：同一个有副作用的操作，执行多次和执行一次，效果得一样。 幂等，才是把「至少一次」补成「恰好一次」的那一环。\ndef deduct(self, sku, order_id): key = f\u0026#34;deduct:{order_id}:{sku}\u0026#34; if key in self.side_effect_log: # 这笔订单已经扣过 return {\u0026#34;status\u0026#34;: \u0026#34;ALREADY_DONE\u0026#34;} self.stock[sku] -= 1 self.side_effect_log.append(key) # 记下「做过了」 return {\u0026#34;status\u0026#34;: \u0026#34;DEDUCTED\u0026#34;} 注意这里有两层防线，缺一不可：\ndone_steps（进度里的记录）：恢复时告诉 runtime「这一步做过，跳过」； side_effect_log（副作用自己的记录）：即使 runtime 漏判了、模型重放了，副作用本身也能认出「这笔订单扣过了」，返回 ALREADY_DONE 而不是真扣。 第二层尤其重要——因为它对应的是真实世界：数据库里的扣款记录、消息队列里的已发送标记，这些「外部世界的痕迹」才是「不重复」的最终兜底。光靠进程内的进度记录，护不住「进程都重启了」的场景——所以必须有一层落在外面的世界。\n五、动手实验一次真实的崩溃 光说不练假把式，跑一个真的。任务是「处理订单：查库存 → 扣库存 → 发通知」，我们故意在「扣库存」之后、「发通知」之前把进程杀掉，然后恢复，看它到底会不会重复扣。\n配套代码在 code/agent-tutorial/c06-state-management/（Python，零第三方依赖），三条工具都是带副作用记录的本地夹具：\n工具 作用 副作用 check_inventory 查库存 无（可安全重跑） deduct_inventory 扣库存 有（扣了不可撤销） notify_shipped 发通知 有（发了不可撤销） 跑起来看两条路径：\n# 离线回归（4 项测试，覆盖 checkpoint 往返、正常跑、崩溃恢复、幂等重放） cd code/agent-tutorial/c06-state-management/python python3 -m unittest test_state_management.py -v # 演示脚本：正常跑完 + 崩溃恢复 python3 state_management.py 这是崩溃恢复那段最想让你看到的输出：\n===== 路径 2：崩溃恢复（发通知前崩溃）===== --- 崩溃前状态 --- step tool=check_inventory observation=SUCCESS step tool=deduct_inventory observation=DEDUCTED checkpoint_saved crashed_here status=RUNNING done_steps=[\u0026#39;deduct_inventory:...\u0026#39;] --- (进程崩溃) --- --- 恢复后状态 --- recovered_from_checkpoint step tool=notify_shipped observation=NOTIFIED checkpoint_saved model_final status=COMPLETED done_steps=[...扣库存..., ...发通知...] deduct_count=1 notify_count=1 看最后一行：deduct_count=1——扣库存只发生了一次。尽管进程崩了、恢复了、模型可能想重放，但扣库存没有被重复执行。notify_count=1——恢复后，漏掉的发通知被补上了。\n这证明的不是「进度能存」，而是**「断了能接回来，而且接回来的时候不会把已经做过的事重做一遍」**——这正是生产级 Agent 和 demo 的根本区别。\n六、总结和下一篇 到这里，第一节列的那几条毛病，解决得差不多了：进度摆到明面上（看得清）、关键步骤落盘（断得起）、副作用幂等（不重做）。循环从「只能一口气跑完」，变成了「断了能接着跑」。\n但回头再看一眼我们这半天的功夫——它解决的，始终是「这一笔订单 O-1001」从开始到结束的进度。而真实世界里，Agent 不会只处理一笔订单：它今天处理一百笔、明天再处理一百笔。于是有个更实际的问题冒出来了：\n处理第 1 笔订单时，Agent 学到「这个 SKU 经常缺货，得先查库存」；到第 100 笔，它还记得吗？还是每一笔都像第一次那样，从头摸索一遍？ 上一笔订单处理到一半崩了，靠落盘接回来了——这是「这一笔」的进度。但「过去一百笔攒下的经验」，存在哪？进程一重启，是不是也一起没了？ 「记住这一次任务走到哪了」，和「记住过去做过的那些事、攒下的那些经验」，是两码事。前者这一篇解决了；后者——让 Agent 跨任务、跨会话，还记得以前的事——正是第七话要讲的：记忆。\n演示代码：code/agent-tutorial/c06-state-management/（Python：state_management.py + test_state_management.py，零第三方依赖，4 项离线测试）。崩溃恢复与幂等为离线确定性夹具验证，不依赖外部 API。\n","date":"2026-05-12T00:00:00Z","image":"/images/c06-state-management-cover.png","permalink":"/post/c06-state-management/","title":"第六话｜状态管理：循环怎么记得住、断得了、恢复得回来"},{"content":"第四话结束，我们手里有了一个能安全执行单次工具调用的 Runtime：模型说\u0026quot;调 get_weather\u0026quot;，代码校验、执行、把结果回喂，模型再总结成一句话。\n这是\u0026quot;会做\u0026quot;了。但它藏着一个更深的问题，上一话没来得及展开：模型的\u0026quot;想\u0026quot;和\u0026quot;做\u0026quot;，还是断开的。\n模型提一次请求，代码执行一次，结束。中间没有\u0026quot;想\u0026quot;——没有让模型停下来，根据刚拿到的结果，再决定下一步。\n本篇就把这两件事接起来。先记住一句话，它是全文的锚：\n光想不做，会空转；光做不想，会盲目。ReAct 做的，就是让\u0026quot;想\u0026quot;和\u0026quot;做\u0026quot;交替起来。\n一、光想不做、光做不想，各有什么毛病 先把两种\u0026quot;缺一半\u0026quot;的形态说清楚。ReAct 就是为了治这两种病而生的。\n光想不做（Chain-of-Thought 的局限）\n让模型一路推理，它每一步都靠自己的记忆往下推。问题是：它推理时不接触外部世界，没有办法去查、去验证。推到一半，遇到它不知道的事实，它不会停下来说\u0026quot;我不知道，去查一下\u0026quot;，而是接着编。错了还往下推，越推越偏。\n这就是\u0026quot;幻觉\u0026quot;和\u0026quot;错误传播\u0026quot;：模型在脑子里空转，没有真实反馈把它拉回现实。\n光做不想（纯工具调用）\n反过来，模型只会调工具，但不动脑。它拿到一个结果，不知道这个结果意味着什么、下一步该干嘛、什么时候该收手。盲目地执行，却不解释为什么。\n一句话立住：想，缺真实反馈会空转；做，缺推理会盲目。 两种形态各缺一半，ReAct 要做的，是把两半合起来。\n二、ReAct 的答案：把\u0026quot;想\u0026quot;和\u0026quot;做\u0026quot;交替起来 2022 年的一篇论文把这个想法抽象成了模式，叫 ReAct（Reasoning + Acting），arXiv 2210.03629。\n想法很朴素：让模型交替地\u0026quot;想一步、做一步、看一步\u0026quot;。\n要素 是什么 谁负责 Thought（想） 模型推理：我知道什么、还缺什么、下一步干嘛 模型 Action（做） 调用某个工具，或给出最终答案 模型提出，代码执行 Observation（看） 工具执行后返回的真实结果 代码 关键在那个\u0026quot;看\u0026quot;。Observation 是外部世界的真实反馈，它把模型的\u0026quot;想\u0026quot;从自己的幻觉里拉回现实。 这是 CoT 缺的那一环——CoT 一路想下去，没有地方停下来\u0026quot;看一眼真实世界\u0026quot;。\n于是循环成立：想一步 → 做一步 → 看一眼真实结果 → 再想一步。想的给做的指方向，做的给想的补事实。这就是\u0026quot;推理和行动交替\u0026quot;。\nflowchart LR A[问题 + 历史] --\u003e B[模型\n想：下一步干嘛] B --\u003e C{要动手?} C --\u003e|是| D[代码执行\n拿到真实结果] D --\u003e E[写回历史\n给模型看] E --\u003e B C --\u003e|否| F[给出最终回答\n结束]有两点要划清，避免误读：\n第一，本文不把任何供应商的 reasoning_content 字段当成论文里的 Thought。论文的 Thought 是文字推理轨迹，现代 API 的 reasoning 字段是另一回事。这篇只验证一个更小、更可观察的问题：模型的下一步动作，是不是真的随观察到的真实结果改变。\n第二，ReAct 并没有在所有任务上都赢过 CoT。论文配套结果里，HotpotQA 6-shot 下 ReAct 是 27.4，CoT 是 29.4，两者结合是 35.1。别把\u0026quot;ReAct 更强\u0026quot;当成普适结论——它带来的是外部反馈回路，不是万能药。\n关键洞察：ReAct 的突破，不是\u0026quot;模型更聪明了\u0026quot;，而是给模型的推理接上了外部反馈——每一步都踩在真实工具结果上，而不是模型自己的幻觉上。\n三、工程上怎么承载：一个循环外壳 \u0026ldquo;想和做交替\u0026quot;这件事，落到代码上，就是一个循环：\nfor step in range(1, max_steps + 1): turn = model.decide(state.messages, tools) # 模型\u0026#34;想\u0026#34;：下一步干嘛 if not turn.tool_calls: # 不想动手了 → 结束 return state observation = execute(turn.tool_calls) # 代码\u0026#34;做\u0026#34;：执行，拿到真实结果 state.messages.append(observation) # 把\u0026#34;看\u0026#34;到的写回历史 循环是外壳，交替是灵魂。不是套个 while 就完事——有三件事，是让这个外壳真正立起来的前提：\n1. 模型没有记忆，每次都得喂全。 模型每次调用都是独立的，不记得上一轮干了什么。所以循环要自己维护消息历史，每一轮把\u0026quot;问题 + 之前做过的 + 看到的结果\u0026quot;完整交给模型。它不是\u0026quot;记得\u0026rdquo;，是被每次提醒。\n2. 循环必须有出口。 两个出口：模型不再调工具（直接给最终回答）→ 正常结束；到达 max_steps 上限 → 兜底强制停止。缺了后者，模型偶尔会陷入\u0026quot;反复调同一个工具\u0026quot;的怪圈，把额度烧穿。\n3. Observation 必须真实，绝不让模型自己编。 这是 ReAct 的生命线。如果模型能自己写\u0026quot;北京天气 25℃\u0026quot;，它就会偷懒编造。Observation 必须来自工具的真实执行结果，代码负责，模型无权编。在原生 tool_calls 里，这一点由 API 层保证：结果以 role=tool 消息回传，模型只能基于它推理。\n这三件事，是\u0026quot;循环外壳怎么搭\u0026quot;的注意点，不是 ReAct 的主角。主角永远是那句：让\u0026quot;想\u0026quot;和\u0026quot;做\u0026quot;交替，让每一步想都踩在真实的\u0026quot;看\u0026quot;上。\n四、看一次真实的\u0026quot;交替\u0026quot; 光说不够，跑一个真的。任务是\u0026quot;公司总部今天的天气\u0026quot;——它天然需要两步：先解析\u0026quot;公司总部\u0026quot;是哪个城市，再用这个城市查天气。\n配套代码在 code/agent-tutorial/c05-react-agent/，Python 和 Java 双份，工具是三个本地只读夹具（不依赖外部 API，避免网络和额度污染控制流证据）：\n工具 作用 行为 resolve_company_address 解析总部地址 上海总部→上海，北京总部→北京，其余→待澄清 get_weather 查城市天气 上海→小雨 22℃，北京→晴 18℃ request_clarification 信息不足时请求补充 无副作用 真实模型跑出来的对照 Trace，是这篇最想让你看到的：\n上海：resolve_company_address({\u0026#34;company_name\u0026#34;: \u0026#34;上海总部\u0026#34;}) → 看到 RESOLVED(city=\u0026#34;上海\u0026#34;) → get_weather({\u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34;}) 北京：resolve_company_address({\u0026#34;company_name\u0026#34;: \u0026#34;北京总部\u0026#34;}) → 看到 RESOLVED(city=\u0026#34;北京\u0026#34;) → get_weather({\u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;}) 注意看第二步：模型\u0026quot;想\u0026quot;出的下一步，参数随它\u0026quot;看\u0026quot;到的结果改变——看到\u0026quot;上海\u0026quot;就查上海，看到\u0026quot;北京\u0026quot;就查北京。\n这比\u0026quot;最后回答换了城市\u0026quot;强得多。它证明的不是\u0026quot;循环多跑了几步\u0026quot;，而是模型真的在根据 Observation 调整下一步的\u0026quot;想\u0026quot;——这正是 ReAct 和\u0026quot;固定脚本多跑几遍\u0026quot;的分水岭。\n跑起来：\n# 离线回归，不需要 Key cd code/agent-tutorial/c05-react-agent/python python3 -m unittest test_react_agent.py # 真实模型路径，Key 只从环境变量读 export LLM_API_URL=\u0026#39;...\u0026#39; LLM_API_KEY=\u0026#39;...\u0026#39; LLM_MODEL=\u0026#39;...\u0026#39; python3 react_agent.py 五、这一篇停在哪，下一篇往哪走 到这里，ReAct 的\u0026quot;交替\u0026quot;讲清了：模型想一步、做一步、看一步，每一步想都踩在真实结果上。\n但有一个口子，正好是下一篇的入口。注意第三节那个循环，每一轮都把完整历史喂给模型。历史一长，问题就来了：\n每次都喂全，Token 越来越贵，也塞不下； \u0026ldquo;看\u0026quot;到的结果、Run 当前走到哪一步，都只在内存里——进程一重启，全没了。 模型\u0026quot;想\u0026quot;和\u0026quot;做\u0026quot;的交替立起来了，但**\u0026ldquo;记得住、断得了、恢复得回来\u0026quot;这件事还没解决**。这是第六话要讲的：状态怎么显式存下来、怎么 checkpoint、怎么在崩溃后接着跑。\n演示代码：code/agent-tutorial/c05-react-agent/（Python：react_agent.py + test_react_agent.py；Java 21 等价实现见 java/）。真实模型路径只从 LLM_API_URL / LLM_API_KEY / LLM_MODEL 环境变量读配置，密钥不进代码、不进 Trace。 参考：ReAct 论文 arXiv 2210.03629；Google Research 官方介绍 React: Synergizing Reasoning and Acting in Language Models；Trace 为 2026-09 本机实测（Python 与 Java 双跑）。\n","date":"2026-04-28T00:00:00Z","image":"/images/c05-react-agent-cover.png","permalink":"/post/c05-react-agent/","title":"第五话｜ReAct：让模型把「想」和「做」接起来"},{"content":"第三话结束时，我们留下一个问题没解决：\ntool_calls 永远是候选，不是命令。谁来判断\u0026quot;能不能执行、怎么执行\u0026quot;？\n这就是第四话要回答的。模型提出的工具调用，哪怕 JSON 合法、参数齐全，也可能是错的、越权的、有副作用的。Runtime 不能照单全收——它得先校验，再执行，而且要在执行出问题时不把局面搞得更糟。\n这里最危险的，不是选错工具，而是一个有副作用、结果又说不清的操作。比如模型要调 refund_order 退款：\nhandler 执行到一半超时了。钱到底退了没有？直接重试，可能重复退款；不重试，用户又以为没退成。\n这一话就沿着这条退款主线，讲清楚 Runtime 怎么把\u0026quot;模型说要调用\u0026quot;变成\u0026quot;安全地执行\u0026quot;——以及最关键的，超时之后怎么根据真实状态做决定，而不是靠猜。\n一、模型说\u0026quot;调 refund_order\u0026quot;，Runtime 先干什么？ 模型返回的 tool_calls 里，是一段结构化申请单：工具名 refund_order、参数 order_id 和 idempotency_key。但它是模型生成的，是外部输入，不是已经校验过的命令。\nRuntime 拿到它，第一件事不是执行，是问三个问题：\n这个工具名，是允许执行的吗？ 参数齐全、类型对吗？ 这个工具现在可用吗？ 任何一个不过，就不进 handler，而是返回一个结构化的错误，让上层能稳定地分支处理。\n为什么要结构化，不能是一段\u0026quot;调用失败，请稍后再试\u0026quot;的文案？因为文案里没有可编程的信息——程序没法可靠地区分\u0026quot;工具不存在\u0026quot;和\u0026quot;参数缺了\u0026quot;和\u0026quot;执行到一半挂了\u0026quot;。而这三种情况，处理方式完全不同：\n错误码 发生了什么 该怎么处理 UNKNOWN_TOOL 模型报了个不存在的工具名 拒绝，修正工具选择 INVALID_ARGUMENT 参数不满足契约 补参数或拒绝 TOOL_UNAVAILABLE 工具已下线或不健康 告知不可用，不能硬调 EXECUTION_ERROR handler 或下游执行失败 按恢复语义决定，见下一节 前三个错误，handler 根本没执行——它们发生在执行之前。第四个才是真正进到了执行阶段，也是最麻烦的。\n二、执行时出了问题，最怕的是\u0026quot;结果说不清\u0026quot; refund_order 通过了校验，handler 真的去调订单服务了——然后超时。\n超时意味着什么？意味着\u0026quot;结果不确定\u0026quot;。请求可能压根没到达订单服务，也可能已经到达、退款都成功了，只是响应没传回来。你分不清。\n这就是最危险的地方：如果 Runtime 把\u0026quot;超时\u0026quot;直接当成\u0026quot;没执行\u0026quot;，再调一次，就可能退了两笔钱。而如果当成\u0026quot;失败了\u0026quot;，用户这边又显示退款失败，但其实钱已经退了——两边对不上。\n所以规则只有一条：超时之后，先查这笔操作的真实状态，不要直接重试。\n怎么查？靠 idempotency_key（幂等键）。它是发起退款时就带上的、这笔操作的唯一标识。领域服务按这个键记录\u0026quot;这笔退款到底执行了没有\u0026quot;，Runtime 超时后先去查它，而不是再发起一笔。\nflowchart TD A[refund_order 超时] --\u003e B[按 idempotency_key 查询真实状态] B --\u003e|SUCCEEDED| C[回放成功结果不再调 handler] B --\u003e|NOT_EXECUTED| D[确认没执行才重试一次] B --\u003e|UNKNOWN / 执行中| E[等待或人工核验]三条分支，对应三种真实状态：\nSUCCEEDED：钱已经退了。Runtime 把成功结果回放给用户，不再调 handler。这是最容易犯错的场景——明明已经成功，却因为\u0026quot;没收到响应\u0026quot;而再退一次。 NOT_EXECUTED：确认没执行过。这时重试一次是安全的。 UNKNOWN / 执行中：状态也查不到。这时既不能重试，也不能谎报成功，只能等待对账或转人工核验。 跑一遍主线 demo，你会看到这样一条 Trace：\nrefund_order response=EXECUTION_ERROR idempotency_status=SUCCEEDED runtime_action=REPLAY_SUCCESS final=SUCCESS refund_order handler_execution_count=1 最后一行是整条链路的关键证据：handler_execution_count=1。它证明 Runtime 虽然经历了\u0026quot;超时 → 查状态 → 回放\u0026quot;，但真正执行退款的 handler 只跑了一次。没有重复退款。\n三、把\u0026quot;谁负责什么\u0026quot;钉死 超时恢复这件事，看着是 Runtime 一个人的活，其实牵涉三个角色，职责不能混：\n角色 负责什么 不负责什么 领域服务（RefundDomain） 记录和查询\u0026quot;退款到底发生没有\u0026quot; 不负责重试策略，也不知道 Runtime 怎么编排 Runtime 按恢复语义，决定回放、重试还是等待 不拥有退款事实，退款有没有发生得问领域 模型 只提出候选调用 不拥有执行权，更不能拿它的话当退款状态 一句话：有副作用工具的超时恢复，首先是领域状态问题，其次才是 Runtime 的重试策略问题。 退款有没有发生，是领域事实，只能问领域服务；Runtime 能做的是照着这个事实去编排下一步，而不是自己猜。\n反过来看，如果领域服务不记录幂等状态，Runtime 再聪明也没用——它没有可以查询的真相，超时之后只能抓瞎。所以幂等这件事，得从工具设计那天就带上，而不是等出了超时才补。\n四、把这条链路跑起来 配套 demo 在 code/agent-tutorial/c04-tool-registry/，Python 和 Java 各一份，语义一致。它不调用 LLM、订单或支付系统——所有 handler 都是本地确定性模拟，所以它验证的是 Runtime 的控制流，不是退款系统的可用性证明。\ncd code/agent-tutorial/c04-tool-registry/python python3 registry.py python3 -m unittest test_registry.py cd ../java gradle run 它覆盖六类场景，每一类都对应上面讲的一个环节：\n场景 验证什么 工具不存在 UNKNOWN_TOOL，不进 handler 工具下线 TOOL_UNAVAILABLE，不能硬调 参数缺失 INVALID_ARGUMENT，补参或拒绝 正常调用 一次执行，SUCCEEDED 超时但已成功 查幂等 → SUCCEEDED → 回放，handler_execution_count=1 超时且未执行 查幂等 → NOT_EXECUTED → 重试一次 其中\u0026quot;超时但已成功\u0026quot;是最值得盯着看的一条：它模拟了 handler 执行成功后、响应却丢失的场景——退款已经发生，SUCCEEDED，但 Runtime 收到的是超时。正确的 Trace 必须是\u0026quot;回放成功结果\u0026quot;，而不是\u0026quot;再退一次\u0026quot;。\n五、还差的：执行成功 ≠ 永远可靠 跑通这条链路，Runtime 能安全地执行一次有副作用的调用了。但这离\u0026quot;可靠\u0026quot;还有距离，几件事本篇点到为止，先立个边界：\n一次超时不等于工具坏了。订单服务偶发抖动，不能立刻把 refund_order 永久下线；连续失败达到阈值才考虑熔断。 熔断后的探测，不能用一笔新退款去试。要用无副作用的健康检查——否则\u0026quot;检查服务恢复没有\u0026quot;这件事本身，又制造了一笔业务动作。 Runtime 不替代鉴权、审批和审计。工具能不能调，除了契约校验，还涉及\u0026quot;这个用户有没有权限\u0026quot;\u0026ldquo;要不要二次确认\u0026quot;\u0026ldquo;留不留审计留底\u0026rdquo;——这些是 Runtime 与领域服务的责任，本篇没展开。 这些都属于\u0026quot;从单次安全执行到生产级可靠\u0026quot;的延伸，先把边界划在这，不抢后续章节的戏。\n到下一话，问题才真正变难 现在，Runtime 已经能把单个候选调用安全地执行，并在超时后不搞砸。\n但它还不会根据第一次工具的结果，决定第二步做什么。\n第五话才进入真正的 ReAct 循环：第二个 Action 必须依赖第一个 Observation。那时，今天建立的 ToolResponse 和幂等恢复，会成为循环里可复用的执行底座。\n","date":"2026-04-14T00:00:00Z","image":"/images/c04-tool-registry-cover-v2.png","permalink":"/post/c04-tool-registry/","title":"第四话｜模型说调 refund_order，Runtime 怎么不把事情搞砸？"},{"content":"上一话跑通了 Function Calling 协议，但留了个更根本的问题没收尾：\n模型凭什么知道该从 tools 菜单里挑 get_weather、又凭什么把“北京”填进 city？\n不是提示词写得好。协议只是给了模型一个“填在哪里”的位置，真正让模型知道“填什么”的，是它的训练。这一话就追这件事：模型是怎么学会“提出一次工具调用”的。\n先给结论，后面再拆：工具调用不是模型的内置能力，而是训练出来的“输出倾向”。训练改变的是它生成某些 token 序列的概率，不是给它装了一个真的会退款的函数。\n一、模型学的，仍然是下一个 token 模型里没有一个叫“退款”的函数。它有的，只是“给定上文，预测下一个词”的能力。所谓“会调工具”，是它学会了：当上下文里出现“退款意图 + 工具定义”时，下一个该输出的，是 refund_order 和 order_id 这几个 token。\n训练样本把它要学的样子，直接摆出来：\n上下文：订单 O-100 已支付，用户要求退款；可用工具有 refund_order(order_id) 目标：refund_order({\u0026#34;order_id\u0026#34;:\u0026#34;O-100\u0026#34;}) 反复见这样的样本，模型参数里就长出一套倾向：“退款 + 已支付”这类上下文，会抬高 refund_order 的概率；订单号 O-100，会抬高被填进 order_id 的概率。\n注意，训练集还得有反例，否则模型会养成坏习惯：\n用户只问配送时间 → 直接回答，不调工具； 订单状态不明 → 先追问，不调工具。 少了这些负样本，模型就会变成“看见什么都想调工具”。\nflowchart LR A[训练样本意图、工具定义、目标调用] --\u003e B[模型参数形成输出倾向] C[本次请求用户消息、当前工具、订单状态] --\u003e D[模型生成候选 tool_calls] B --\u003e D D --\u003e E[Runtime校验、确认、执行或拒绝]关键洞察：模型只是提出了候选调用。它没有因此获得订单事实、权限，也没有执行权。 会“说”要干什么，和“真的干了什么”，是两码事。\n二、这条“倾向”，是靠什么训练出来的？ 问题来了：这些“上下文 → 正确调用”的样本，是怎么来的？人工一条条标注太贵，学界给了几条公开的路线。\nToolformer 解决的是“数据怎么规模化造出来”。它的做法很有意思——让模型自己当标注员：\n在一段普通文本里，让模型猜哪些位置可能需要调 API，采样出一堆候选调用； 真的去执行这些调用，拿到结果； 关键一步：只保留那些“拿到结果后，模型预测后续文字更准了”的调用——具体说，就是比较“有结果”和“没结果”两种情况下，模型对后面 token 的预测损失，损失降得够多才留下； 用过滤后的样本去微调模型。 一句话：模型自己提出调用、自己执行、再自己判断“这次调用到底有没有用”，有用的才拿来当训练样本。 全程不需要人工标注“这里该不该调工具”。Toolformer\nGorilla 解决的是另一个问题：API 会变。哪怕模型学过 get_weather(city)，文档也可能改成 get_weather(location, unit)。Gorilla 的做法是微调 + 文档检索结合：训练给模型调用能力，检索把“当前最新的接口契约”带进上下文，让它跟得上变化。Gorilla\nCoT 常被顺带提起，但它其实是个对照，不是工具调用训练：它研究的是“在提示里给推理示例能不能改变输出”。它能改变当前这一轮的输出，但不改模型参数——和 Toolformer/Gorilla 那种真正动参数的训练，是两回事。CoT\n三、一件必须分清的事：哪些证据能信，哪些不能 讲到这里，得划一条诚实的边界。关于“模型为什么能调工具”，能拿到的证据分三类，它们不能互相冒充：\n证据类型 能说明什么 不能说明什么 论文公开的方法（Toolformer/Gorilla） “存在这些训练工具调用的公开方法” 不代表某个商业模型就用了它 厂商披露（OpenAI 公告） 0613 模型经过微调、Structured Outputs 靠训练+约束解码 只限定到那几家、那段时间，不能外推 黑盒 API 能观察到的 输入什么、输出什么（比如 description 改了就选错工具） 看不到训练语料、训练阶段、奖励方法 最关键的一条是第三类：你从 API 拿到的结果再稳定，也反推不出厂商到底是怎么训练的。 训练语料长什么样、分几个阶段、有没有用 RLHF——这些是黑盒，除非厂商自己说，否则只能标“未知”。\n所以别犯那个错：看到 tool_calls 稳定输出，就说“这个模型用了 Toolformer”。稳定输出能证明“它训练得很好”，证明不了“它是怎么训练的”。\n四、Prompt-only 和 Native tools，差的不只是“格式” 上一话讲了 Native tools 的协议形态，这里补一层它和训练/推理的关系。\nPrompt-only：在 system prompt 里约定“需要退款时输出 JSON”。模型把 JSON 塞进普通 content 里，Runtime 还得从正文里把命令抠出来——这里混着自然语言、Markdown、可能还有好几个 JSON。\nNative tools：在 API 的 tools 字段传 schema，模型用独立的 tool_calls 字段返回。Runtime 直接拿到工具名、参数和调用 id，不用猜哪里是命令。\n这个差异不是“格式好看点”，而是责任归属变了：Prompt-only 把“保证输出长得像 JSON”的责任甩给了提示词和你的解析器；Native tools 把这层责任接了过去，一部分靠训练（让模型更会按 schema 输出），一部分靠推理阶段的约束解码（生成时卡住，只允许符合 schema 的 token 出来）。\nOpenAI 官方也明确说过：Function Calling 的可靠性，一部分来自模型的微调，一部分来自 Structured Outputs 的 constrained decoding。Structured Outputs\n这里有个真实的坑能说明这层差异。我们 Java 版最初只写了“需要时输出 JSON”，没说清 name 和 arguments.order_id 的形状。结果 Native tools 模式仍能返回结构化调用，Prompt-only 模式却解析不出来。把提示词补成明确的 JSON 模板后，Java 的 8 个场景才全过。这不是“Java 不适合做 Agent”，而是Prompt-only 把输出协议的一部分责任，留在了你的提示词和解析器上——而 Native tools 帮你卸掉了一部分。\n五、动手看一次：description 才是“选哪个”的开关 我们用一套虚构订单做实验，Python 和 Java 21 各跑一遍。Runtime 先注入已验证的订单状态，模型只做一次工具选择，程序只记录候选调用，绝不执行退款或取消。\n四种场景，正确行为是这样：\n场景 正确行为 已支付 O-100，要求退款 refund_order(O-100) 未支付 O-200，要求取消 cancel_order(O-200) 无订单号和状态 追问，不调用 无退款/取消意图 自然语言回答，不调用 基线下两种语言都过。然后做一个故障注入：只把 refund_order 和 cancel_order 的 description 对调，其余全不变——工具名、模型、用户请求、参数 schema 都一样。\n结果很干净：已支付退款稳定变成 cancel_order(O-100)，未支付取消稳定变成 refund_order(O-200)，而订单号仍然是对的。恢复 description 后，回归全过。\n这个实验说明两件事：\n工具选择（选哪个）靠的是 description——模型是读描述来判断“这个工具是干嘛的”，描述写错了，它就选错。 参数提取（填什么）可以和工具选择分开看——订单号还是从用户消息和 Runtime 注入的上下文里来的，两个工具的参数 schema 没变，所以订单号没错。 但这能证明什么、不能证明什么，得说清楚：它证明了“description 影响路由”这个可观察行为，证明不了模型用了哪种训练法。description 写对了，也不代表参数永远填对——Runtime 照样得校验。\n六、所以，能信到什么程度 绕了一圈，回到开头那个问题：模型为什么能生成工具调用？\n因为它被训练成了这样——训练样本让它形成了“意图 + 工具定义 → 结构化调用”的输出倾向。Toolformer、Gorilla 这些公开方法，展示了这条倾向可以怎么规模化地训练出来；厂商还叠加了微调和约束解码，让输出更稳。\n但“会提请求”不等于“可靠”。训练让模型更会“说”要干什么，不会让它“变聪明”——它照样可能选错工具、编造业务参数、请求一个无权执行的动作。tool_calls 永远是候选，不是命令。 谁来判断“能不能执行、怎么执行”，是下一篇的事。\n参考资料：\nWei et al., Chain-of-Thought Prompting Elicits Reasoning in Large Language Models Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools Patil et al., Gorilla: Large Language Model Connected with Massive APIs OpenAI, Function calling and other API updates OpenAI, Introducing Structured Outputs in the API ","date":"2026-03-31T00:00:00Z","image":"/images/c03-function-calling-training-cover-v2.png","permalink":"/post/c03-function-calling-training/","title":"第三话｜模型为什么能生成工具调用？"},{"content":"上一话我们搞懂了两件事：模型只会补全文字；想让它干活，得让它把“想干什么”说成某种格式，再由外面的代码去执行。\n但上一话那个办法很脆——用提示词逼模型输出 JSON，模型经常给你残缺的 JSON、在前后多写几句废话、或者干脆忘了格式。于是就有了 Function Calling：它把“让模型提出一次工具调用”这件事，从碰运气的提示词技巧，做成了一套规规矩矩的协议。\n这一话就讲这套协议：它长什么样，谁先做出来的，以及为什么它能让模型“说清楚要干什么”。\n一、协议要解决的，就是上一话那个“脆” 先看上一话的提示词 hack 是怎么翻车的。你在 system prompt 里写“想查天气就输出 {\u0026quot;tool\u0026quot;:\u0026quot;get_weather\u0026quot;,\u0026quot;city\u0026quot;:\u0026quot;...\u0026quot;}”，然后：\n模型输出的不是纯 JSON，前后还夹着“好的，我帮你查一下”这种话； JSON 本身残缺，少个引号、少个逗号，json.loads 直接崩； 换了个问法，模型就把格式忘得一干二净。 每次翻车，你都得靠更长的提示词、更严的正则去兜，兜到最后还是不可靠。\n协议的意义就在这里：别再用文字去“暗示”模型输出格式，而是给一个结构化的位置，让模型把调用填进去。 这个位置是 API 里明确规定好的，模型要么填对，要么 API 直接拒绝，没有“碰运气”的中间地带。\n二、协议长什么样：tools 进，tool_calls 出 整个协议，一进一出两件事：\n请求里传 tools：你告诉模型“现在有哪些工具可以用”。这是一份菜单，写清楚每个工具叫什么、干什么、参数长什么样。 响应里收 tool_calls：模型告诉你“我想调哪个工具、带什么参数”。这是一张申请单。 下面是一份声明了本地天气工具的菜单：\nTOOLS = [ { \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;查询指定城市的天气演示数据\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;city\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;城市名，如北京\u0026#34;} }, \u0026#34;required\u0026#34;: [\u0026#34;city\u0026#34;], \u0026#34;additionalProperties\u0026#34;: False, }, }, } ] 注意，tools 不是把 Python 函数“上传”给模型。它只是一份描述：函数叫 get_weather、查天气的、需要一个 city 字符串参数。模型拿到的是这些文字，不是那个函数本身。Chat Completions API\n把菜单连同问题一起发过去：\nassistant_message = client.chat.completions.create( model=MODEL, messages=[{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;请查询北京现在的天气。\u0026#34;}], tools=TOOLS, tool_choice=\u0026#34;required\u0026#34;, ).choices[0].message 响应里我们关心的不是自然语言，而是这张申请单：\n{ \u0026#34;id\u0026#34;: \u0026#34;call_00_...\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;arguments\u0026#34;: \u0026#34;{\\\u0026#34;city\\\u0026#34;: \\\u0026#34;北京\\\u0026#34;}\u0026#34; } } 拆开看每个字段：\nid：这条调用的唯一标识，回传结果时要靠它关联； type：固定是 function，标记这是一次函数调用； function.name：想调哪个工具； function.arguments：一个 JSON 字符串——注意它还是字符串，不是对象，代码里得再 json.loads 一层才能用。 它表达的就是一句话：“请调用 get_weather，参数是北京。”到这里，模型既没有运行 Python，也没有拿到天气数据。它只是把“想干什么”用协议规定的格式写清楚了。\n三、这套协议，是谁先做出来的 它不是我编的，也不是模型厂商某天灵机一动。时间线很清晰：\n2022 年，ReAct（Yao et al., arXiv 2210.03629）提出“推理 + 行动”交替，用提示词让模型按 Thought / Action 的格式输出，再让代码去执行； 2023 年 2 月，Toolformer（Meta AI, arXiv 2302.04761）证明“模型自己决定何时调 API”这个能力是能被训练出来的； 2023 年 5 月，Gorilla（UC Berkeley, arXiv 2305.15334）开始微调模型学写 API 调用。 这几篇走的都是“提示词 + 解析”的路线——也就是上一话讲的那个脆办法。\n真正的转折在 2023 年 6 月 13 日：OpenAI 首次发布原生 Function Calling，随 gpt-4-0613 和 gpt-3.5-turbo-0613 两个模型快照一起推出。从这一天起，“让模型提工具调用”不再靠提示词暗示，而是变成了 API 里的标准字段——就是我们第二节看到的 tools 进、tool_calls 出。\n四、别看就一个 tool_calls，背后做了很多工作 有个问题自然冒出来：同样是“让模型输出调用”，为什么提示词 hack 天天翻车，而协议一出来，模型就能稳定地输出合法参数、不再给你残缺 JSON？\n不是因为提示词写得好了。是厂商在背后做了工作——模型是被训练成“会正确提出调用”的，这个能力不是运气，是训练出来的。\n这里只说结论，不展开原理（那是下一话的事）：\n微调：训练阶段喂了大量“工具调用对话”样本，让模型学会判断要不要调、选哪个工具、生成合法参数； 约束解码：生成的时候，在输出层卡住模型，让它只能吐出符合 schema 的内容，从根上杜绝“残缺 JSON”。 这两件事把“正确提出调用”从提示词的玄学，变成了模型本身的能力。至于模型内部到底是怎么学会的、这两件事的具体机制——那是第三话要专门讲的问题，这里先按下不表。\n五、协议只负责“提出”，不负责“执行” 回到那张申请单。tool_calls 拿回来，协议的部分就结束了。接下来是你的代码接手：\ndef execute_tool(name: str, arguments_json: str) -\u0026gt; str: if name != \u0026#34;get_weather\u0026#34;: raise ValueError(f\u0026#34;不允许执行未知工具：{name}\u0026#34;) arguments = json.loads(arguments_json) if not isinstance(arguments, dict) or set(arguments) != {\u0026#34;city\u0026#34;}: raise ValueError(\u0026#34;get_weather 参数必须且只能包含 city\u0026#34;) city = arguments[\u0026#34;city\u0026#34;] if not isinstance(city, str) or not city.strip(): raise ValueError(\u0026#34;city 必须是非空字符串\u0026#34;) return get_weather(city.strip()) 模型输出在这里被当成外部输入处理：先校验工具名，再校验参数，然后才真正执行。25℃ 来自 get_weather() 的返回值，不是模型编的：\ndef get_weather(city: str) -\u0026gt; str: weather = { \u0026#34;北京\u0026#34;: \u0026#34;多云，25℃，东北风3级\u0026#34;, \u0026#34;上海\u0026#34;: \u0026#34;小雨，22℃，东南风2级\u0026#34;, } return weather.get(city, f\u0026#34;暂无{city}的天气演示数据\u0026#34;) 协议让模型“说清楚要干什么”，执行永远在模型外面——这是上一话那条结论的延续，在这里变成了可运行的代码。\n六、把结果交回去，也是协议的一部分 函数执行完，Python 手里有了天气文本，但用户还没看到一句话。模型上一次只停在“我想调什么工具”，所以要把两样东西放回消息历史：\nmessages.append(assistant_message) # 模型提出的调用请求 messages.append( { \u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34;, # 工具执行结果 \u0026#34;tool_call_id\u0026#34;: tool_call.id, # 关联到上面那条 tool_calls \u0026#34;content\u0026#34;: result, } ) role=tool 这一步不是可选的，是协议强制要求的。这里有个真实的坑：有人把 role=tool 写成了 role=user，API 直接报 HTTP 400——\nAn assistant message with \u0026#39;tool_calls\u0026#39; must be followed by tool messages responding to each \u0026#39;tool_call_id\u0026#39;. 意思很直白：你给模型看了一张 tool_calls 申请单，就必须逐条配上一份对应的 tool 结果，用 tool_call_id 对上号。少一条、错一条，协议层面就拒绝，根本轮不到模型来猜。这个报错本身，就是“协议不是约定俗成、而是硬约束”的最好证据。\n补上结果后再问一次模型，它才能把“多云，25℃”组织成一句人话。事实来自函数，句子由模型生成，两边分得清清楚楚。\n七、回到最开始的问题：到底谁调用了工具？ 角色 输入 输出 不负责什么 模型 用户问题与 tools 菜单 tool_calls 申请单 不执行本地函数 你的代码 工具名、参数与本地能力 校验后的工具结果 不应把模型请求原样放行 模型 role=tool 结果 面向用户的回答 不产生工具返回的外部事实 Function Calling 没有把函数“装进模型”。它做的，是把上一话那个脆弱的提示词 hack，变成了一套 tools 进、tool_calls 出的标准协议——模型提请求，你的代码执行，模型再说话。\n跑通一次，你会看到这三步按顺序发生：\n模型请求：get_weather({\u0026#34;city\u0026#34;:\u0026#34;北京\u0026#34;}) 程序执行：多云，25℃，东北风3级 模型回答：北京现在多云，气温 25℃，东北风 3 级。 配套 Python 与 Java 实现在 GYA 仓库与 GYA-Java 仓库的 c02-function-calling 目录，按 README 的命令跑，不复制这篇文章里零散的代码块。\n协议跑通了，但一个更根本的问题还悬着：模型凭什么知道，该从菜单里挑 get_weather、该填北京？ 这已经不是协议能回答的了——得回到模型训练本身。下一话。\n参考资料：\nDeepSeek, Tool Calls DeepSeek, Chat Completions API Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools Patil et al., Gorilla: Large Language Model Connected with Massive APIs ","date":"2026-03-17T00:00:00Z","image":"/images/c02-function-calling-cover.png","permalink":"/post/c02-function-calling/","title":"第二话｜Function Calling：谁把“调用工具”做成了一套协议？"},{"content":"几年前，我还在用 ChatGPT 查技术资料、翻译文档、写文案前先理个大纲。那时候它是“聊天窗口”：我打字，它回文字，聊完就关掉。\n后来出现的 Agent 工具，真的能帮我干活了——写代码、执行命令行、调用 API 查数据。\n从“聊”到“干”，这几年发生了什么？\n要回答这个问题，得先看清一件被很多人忽略的事：大模型从头到尾，只会做一件事。\n模型只会补全文字 不管外面把它包装成聊天助手还是 Agent，模型干的事情始终只有一件：给你一段上文，它预测下一个最可能出现的词，然后把这个词接上去，再预测下一个，一直循环，直到吐出一段完整的文字。\n这个过程有个名字，叫“自回归生成”。它的输入是文字，输出还是文字。\n所以模型天生有几个边界，跟它聪不聪明没关系，是它的结构决定的：\n它没有手，不会真的去点一个按钮、执行一行代码； 它没有网络，不会自己发一个 HTTP 请求去查天气； 它没有文件系统，不会自己创建一个文件、改一行代码。 它唯一能做的，是根据上文，猜下一段文字该怎么写。\n理解了这一点，再回头看那个问题就清楚了一半：一个只会补全文字的模型，是怎么“干起活”来的？\n让它“说”出要干什么 答案藏在一个很朴素的技巧里。\n模型只会写字，那我们就让它把“想干什么”用固定格式写出来。\n比如，我们在提示词里跟模型约定：\n如果你想查天气，就严格输出下面这个格式，别的什么都别写： {\u0026#34;action\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;} 然后用户问“北京今天天气怎么样”。模型还是只会补全文字，只不过这一次，它补全出来的不是一段聊天，而是一行长得很像代码的 JSON：\n{\u0026#34;action\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;} 到这里，模型仍然什么都没做。它只是“说出”了：我想调 get_weather，参数是北京。\n真正动手的是模型外面的程序。它拿到这行 JSON，解析出 action 是 get_weather、city 是北京，然后自己去调用真正的查天气函数：\nimport json, re # 模型输出的那行文字 model_output = \u0026#39;{\u0026#34;action\u0026#34;: \u0026#34;get_weather\u0026#34;, \u0026#34;city\u0026#34;: \u0026#34;北京\u0026#34;}\u0026#39; # 程序解析它 data = json.loads(model_output) if data[\u0026#34;action\u0026#34;] == \u0026#34;get_weather\u0026#34;: result = get_weather(data[\u0026#34;city\u0026#34;]) # 真正查天气的是这行代码 拿到天气结果后，程序再把这个结果塞回给模型，模型补全成一句人话：“北京今天多云，25 度”。\n看明白了吗？整条链路里，模型从头到尾没执行过任何东西。它只是把“想干什么”用固定格式说了出来，动手的是外面那段解析加执行的代码。\n看起来模型“长出了手脚”，其实是外面套了一双手。\n这套做法，其实是 Function Calling 的前身 上面那套“提示词逼出格式、代码解析执行”的做法，不是谁凭空想出来的玩具。在原生 Function Calling 出现之前，它是开发者让模型调用工具的标准办法——先用提示词约定好输出格式，再用正则表达式或者 JSON 解析器去抠出工具名和参数。\n它好用，但也很脆。模型会输出残缺的 JSON、在 JSON 外面多写一句“好的，我帮你查”、或者干脆忘掉约定格式。这些问题，逼着人们后来把这件事从“提示词技巧”升级成了“协议”——也就是第二话要讲的 Function Calling。\n但不管怎么升级，底层逻辑从没变过：\n模型只负责用固定的格式，把“要干什么”说清楚；真正的执行，永远发生在模型外面。\n想通这一点，整个 Agent 世界就有一把钥匙了。你之后会看到的所有花活——多步循环、记忆、MCP、Skill——都在做同一件事的不同部分：让模型把意图说清楚，让外面那套代码把事做成，再把结果喂回去。\n而外面这套代码，我们给它起了个名字，叫 Runtime。它才是 Agent 真正“长”出来的那只手。\n下一篇，我们就从这套做法的标准化形态开始：模型怎么用一套协议，规规矩矩地提出一次工具调用——Function Calling。\n配套 Python 与 Java 实现在 GYA 仓库与 GYA-Java 仓库的 c01-chat-only 目录，按 README 的命令跑，不复制这篇文章里零散的代码块。\n参考资料：\nOpenAI, Function Calling Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models ","date":"2026-03-03T00:00:00Z","image":"/images/c01-chat-only-model-cover.png","permalink":"/post/c01-chat-only-model/","title":"第一话｜大模型只会补全文字，Agent 的“手脚”是怎么来的？"},{"content":"笔者在过去主要使用的JDK版本是JDK8，最近在学习最新的JDK长期支持版本——JDK21，本文将简要记录一下JDK21相比于JDK8的一些新特性，对于一些比较有意思的新特性也会在后续文章中逐步展开研究。\nvar 关键字局部变量推断 // 仅限局部变量 var name = \u0026#34;zhangsan\u0026#34;; var age = 18; var isMan = true; var list = new ArrayList\u0026lt;String\u0026gt;(); var map = new HashMap\u0026lt;String, String\u0026gt;(); switch 表达式增强 // 假设有枚举定义为 public enum Direction { EAST, SOUTH, WEST, NORTH, NORTHWEST, ; } // switch 表达式为变量赋值 var witchCity = switch (direction) { case EAST, NORTHWEST -\u0026gt; \u0026#34;成都\u0026#34;; case WEST -\u0026gt; \u0026#34;上海\u0026#34;; case SOUTH -\u0026gt; \u0026#34;广州\u0026#34;; case NORTH -\u0026gt; \u0026#34;北京\u0026#34;; default -\u0026gt; throw new IllegalArgumentException(\u0026#34;Invalid direction: \u0026#34; + direction); }; // yield 关键字返回值，并跳出switch表达式（相当于break） var witchCity = switch (direction) { case EAST, NORTHWEST: System.out.println(\u0026#34;成都\u0026#34;); yield \u0026#34;成都\u0026#34;; case WEST: System.out.println(\u0026#34;上海\u0026#34;); yield \u0026#34;上海\u0026#34;; case SOUTH: System.out.println(\u0026#34;广州\u0026#34;); yield \u0026#34;广州\u0026#34;; case NORTH: System.out.println(\u0026#34;北京\u0026#34;); yield \u0026#34;北京\u0026#34;; default: system.out.println(\u0026#34;未知\u0026#34;); yield \u0026#34;未知\u0026#34;; }; // yield 关键字结合lambda表达式 var witchCity = switch (direction) { case EAST, NORTHWEST -\u0026gt; { System.out.println(\u0026#34;成都\u0026#34;); yield \u0026#34;成都\u0026#34;; } case WEST -\u0026gt; { System.out.println(\u0026#34;上海\u0026#34;); yield \u0026#34;上海\u0026#34;; } case SOUTH -\u0026gt; { System.out.println(\u0026#34;广州\u0026#34;); yield \u0026#34;广州\u0026#34;; } case NORTH -\u0026gt; { System.out.println(\u0026#34;北京\u0026#34;); yield \u0026#34;北京\u0026#34;; } default -\u0026gt; { System.out.println(\u0026#34;未知\u0026#34;); yield \u0026#34;未知\u0026#34;; } }; 记录类（record class） 记录类的使用场景是存储数据，尤其是那些纯粹做数据传输的对象如DTO等，其每个字段都是不可变的。\n// 定义一个记录类,标注了record关键字，同时指定了name和age两个字段，编译器已经自动为其生成了构造函数、字段访问方法name()和age()。 // 除字段不能被修改、不能继承其他类、不能添加实例字段之外其他的特性与普通类相同。 public record Student(String name, int age) { // 不能添加实例字段 // private String teacher; // 构造器可以自定义 public Student { if (age \u0026lt; 0) { throw new IllegalArgumentException(\u0026#34;年龄不能小于0\u0026#34;); } if (name == null || name.isEmpty()) { throw new IllegalArgumentException(\u0026#34;姓名不能为空\u0026#34;); } } // 可以定义方法 // 可以实现接口 } 密封类（Sealed Classes） 密封类的使用场景是定义一个类的继承规则，只有指定的类可以继承该类，其他类不能继承该类。类之间耦合更紧了，但是也提升了类型安全。在编写三方库提供给别人用又不希望被使用者随意继承使用时这个特性会特别有用。\n// sealed关键字定义一个密封类，同时permits关键字指定了可以继承该类的类 public sealed class Job permits Author, Teacher { public String name() { return \u0026#34;renxin\u0026#34;; } } // 定义一个final类为密封类的子类，该类不能被继承 public final class Author extends Job { public String name() { return \u0026#34;renxin\u0026#34;; } } // 定义一个non-sealed修饰的类为密封类的子类，该类放弃密封性可以被普通类继承 public non-sealed class Author extends Job { } 子类必须与密封类处于同一个模块（或同一个包，如果没有模块系统）\n子类必须显式继承密封类，否则编译不通过\n密封类的子类必须被显式声明为final、sealed或non-sealed。这是密封类设计的核心原则。这样做有两方面的好处。\n明确类的继承结构，防止继承链失控 与模式匹配结合可以在编译期做到穷举检查 // Author类、Teacher被明确定义为final类，编译期可以知道该类被哪些子类继承，做到穷举检查 public abstract sealed class Job permits Author, Teacher {} public final class Author extends Job {} public final class Teacher extends Job {} void handle(Job job) { switch (job) { case Author a -\u0026gt; System.out.println(\u0026#34;Author\u0026#34;); case Teacher t -\u0026gt; System.out.println(\u0026#34;Teacher\u0026#34;); } // 编译通过，若缺失case Teacher分支，编译错误 虚拟线程（Virtual Threads） 虚拟线程是一种用户态的轻量级线程，类似于其他语言的协程，它的显著提高了Java程序的并发支持能力。细节请参考笔者另一篇文章：理解虚拟线程\n新的HTTP客户端API 支持同步调用和异步调用，异步调用使用CompletableFuture来返回结果。\n// 同步调用 HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(\u0026#34;https://httpbin.org/get\u0026#34;)) .GET() .build(); HttpResponse\u0026lt;String\u0026gt; response = client.send(request, HttpResponse.BodyHandlers.ofString()); // 异步调用 CompletableFuture\u0026lt;String\u0026gt; stringBodyCompletableFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body); instance of 模式匹配 instance of 语法增强，可以省去类型转换步骤。\n// jdk8写法 String obj = \u0026#34;read.renxinblog.cn\u0026#34;; if(obj instanceof String){ String s = (String) obj; System.out. println(s.length()); } // jdk21写法 var obj = \u0026#34;read.renxinblog.cn\u0026#34;; if(obj instanceof String s){ System.out. println(s.length()); } 新一代GC：ZGC 相较于上一代的G1，ZGC的性能更加优秀，同时也更加稳定。\n几乎所有GC阶段都并发进行，STW时间极短，这意味着GC暂停对应用程序造成的性能波动非常小。 能够支持更大的堆，最多达16TB，并且随着堆的增大，GC性能几乎没有降低，这对于数据密集型的大堆系统非常有用。 ZGC能够保证GC停顿时间可控，更容易实现系统的SLO要求。 更高的并发能力同时也意味着更高的CPU消耗。 总之，ZGC以复杂的技术实现换来了极低的延迟时间和大堆性能表现，是G1控制延迟的进化版。性能的提升也带来更高的CPU消耗，但这不是一个缺点，而是一个非常的选择。\n其他 其他特性还包括Stream API的增强、集合工厂方法、文本块（字符串增强）、日期API增强等，这里不多做介绍。\n","date":"2025-04-21T00:00:00Z","image":"/images/java-jdk21-new-features-cover.png","permalink":"/post/java/jdk21-new-features/","title":"JDK21新特性"}]