AI Agents in Action · 第十章

把「思考」
拆成零件

这是全书最像论文的一章:它把智能体按认知科学的方式拆成七个模块—— 感知、规划、执行、评估、注意力、记忆——每个模块都是一个独立的 agent, 它们围着同一块「黑板」读写。这就是第一章提过的黑板模式, 到这里才给出完整实现。 而这一章最诚实的部分在最后三节:让智能体知道自己不知道。

02 · 感知
这问题是啥类型、多复杂、哪里有歧义
03 · 规划
该用哪种策略、拆成哪些子目标
04 · 执行
调工具,把子目标做掉
01 · 认知工作区
所有模块共读共写的那块黑板。
它就是这个智能体的「当前意识」。
raw_query · task_type
current_strategy · sub_goals
findings · memory_hits
confidence · confidence_trend
steps_taken · failed_approaches
active_signal
05 · 评估
这轮结果够不够好、信心该升还是降
06 · 注意力
现在最该关心哪件事
07 · 记忆
以前见过类似的吗
10–12 · 元认知
我信心够吗?我卡住了吗?
我是不是根本不知道?

没有一个模块直接跟另一个说话——它们只跟黑板说话。这是整章的架构。

动手之前 —— 这一章有件事得先讲明白

这一章的旗舰成品,作者自己从没跑过

09_cognitive_agent.py775 行,它的 docstring 自称是「把清单 10.1–10.8 组装成一个完整可运行的系统」。 但它从来没有被真正运行过。我有两条独立证据。

证据一:作者忘了删的 chapter_10/.claude/settings.local.json 这是 Claude Code 的命令白名单,忠实记录了作者到底跑过什么:

python 10_confidence_gate.py                 ← 真跑过
python 11_stagnation_detector.py            ← 真跑过
python 12_boundary_awareness.py             ← 真跑过
ast.parse(open('09_cognitive_agent.py'))  ← 只做了语法检查
ast.parse(open('08_cognitive_loop.py'))   ← 只做了语法检查

整章最大的两个文件(775 行和 488 行),验证方式是「能不能通过 Python 语法解析」。

证据二:它引用了一个不存在的 npm 包。09 第 230 行写着 @anthropic/brave-search-mcp——这正是第九章那个包。 我查过 npm registry:404,@anthropic 这个 scope 下没有任何公开包要是作者跑过一次,几秒钟内就会撞上这个 404。

所以怎么办?101112 当这一章的主菜——它们是经过验证的、能跑的、而且是本章最有价值的思想09 当架构参考读,别指望它开箱能跑。

必须 cd 进去跑 —— 和第六章正好相反

04_execution_module.py 把 filesystem server 的沙箱写成了 "./docs"——相对于当前工作目录。所以这一章必须:

cd chapter_10          # ← 必须先进来,否则找不到 ./docs
python 10_confidence_gate.py

第六章要求你在仓库根目录跑,这一章要求你 cd 进来—— 全仓库没有任何一个 cwd 能跑通所有章节。 顺带一提,那个 docs/ 目录里只有一个 55 字节README.txt,一句话而已,就是个给 filesystem server 用的靶子。

第一部分 · 黑板

先有一块共享的黑板

七个模块不直接对话,全靠往同一个对象上读写。这个对象就是整章的地基。

认知工作区:这个智能体的「当前意识」

对应清单 10.1 · 01_cognitive_workspace.py(66 行,纯数据结构)

from enum import Enum

from pydantic import BaseModel, Field


class TaskType(str, Enum):
    # 枚举值保持英文 —— 它们是代码里比较的字面量
    SIMPLE_LOOKUP = "simple_lookup"      # 简单查找
    MULTI_STEP = "multi_step"            # 多步骤
    CONTRADICTORY = "contradictory"      # 自相矛盾
    AMBIGUOUS = "ambiguous"              # 有歧义
    COMPOSITIONAL = "compositional"      # 需要组合
    UNKNOWN = "unknown"


class StrategyType(str, Enum):
    DIRECT = "direct"                    # 简单问题直接做(系统 1)
    DECOMPOSE = "decompose"              # 多步问题拆成子任务
    EXPLORE = "explore"                  # 先探索收集信息再下手
    HYPOTHESIS_TEST = "hypothesis_test"  # 检验几个互相竞争的假设


class AttentionSignal(str, Enum):
    NONE = "none"
    LOW_CONFIDENCE = "low_confidence"    # 信心不足
    STAGNATION = "stagnation"            # 卡住了
    CONTRADICTION = "contradiction"      # 发现矛盾
    KNOWLEDGE_GAP = "knowledge_gap"      # 知识盲区
    TASK_COMPLETE = "task_complete"      # 做完了


class Finding(BaseModel):
    content: str                         # 查到的内容
    source: str                          # 从哪来的
    relevance_score: float = 0.0         # 和子目标的相关度
    quality_note: str = ""               # 对结果质量的顾虑


class CognitiveWorkspace(BaseModel):
    # 任务表征 —— 感知模块写
    raw_query: str = ""
    task_type: TaskType = TaskType.UNKNOWN
    extracted_entities: list[str] = Field(default_factory=list)
    complexity_estimate: float = 0.0     # 0.0(很简单)到 1.0(非常复杂)
    ambiguities: list[str] = Field(default_factory=list)

    # 当前假设 —— 规划模块写
    current_strategy: StrategyType = StrategyType.DIRECT
    sub_goals: list[str] = Field(default_factory=list)
    alternative_strategies: list[str] = Field(default_factory=list)

    # 中间结果 —— 执行和记忆模块写
    findings: list[Finding] = Field(default_factory=list)
    memory_hits: list[str] = Field(default_factory=list)

    # 信心状态 —— 评估模块写
    confidence: float = 0.5              # 从中立开始,往 1.0 或 0.0 走
    confidence_trend: list[float] = Field(default_factory=list)

    # 执行历史
    steps_taken: list[str] = Field(default_factory=list)
    failed_approaches: list[str] = Field(default_factory=list)
    iteration_count: int = 0

    # 注意力信号 —— 注意力模块写
    active_signal: AttentionSignal = AttentionSignal.NONE
    signal_source: str = ""
这块黑板上有两个字段是这一章的灵魂

confidence_trend(信心走势)——注意它是个列表, 不是单个数。因为「我现在有多确定」不重要,「我的确定程度在往哪个方向走」才重要。 信心从 0.8 掉到 0.5,和从 0.2 涨到 0.5,是完全不同的处境。

failed_approaches`(失败过的路子)——记录什么行不通。 前九章的智能体从不记这个,所以它们会反复撞同一堵墙

这两个字段就是「认知」和「能干」的分界线:能干的智能体知道自己在做什么, 有认知的智能体知道自己做得怎么样

和第九章一样:这个文件跑不出任何东西

66 行全是类定义,没有 main()、没有 __main__0108 八个文件都是这样—— 它们是给后面抄的骨架,也是给书排版用的。 全章只有 09101112__main__

七个模块,一个模子刻出来的

对应清单 10.2–10.8 · 0207

看完一个就看完了全部——每个模块都是「一个 agent + 一个 output_type」。 以感知模块为例:

from enum import Enum

from agents import Agent
from pydantic import BaseModel, Field


class TaskRepresentation(BaseModel):
    task_type: TaskType
    extracted_entities: list[str] = Field(default_factory=list)
    complexity_estimate: float = 0.0
    ambiguities: list[str] = Field(default_factory=list)


perception_agent = Agent(
    name="Perception",
    instructions="""你是一个认知智能体的感知模块。
    你的工作是分析用户的查询,在采取任何行动之前,
    先产出一份对这个任务的结构化理解。

    对每个查询,判断:

    1. task_type:这是 simple_lookup、multi_step、contradictory、
       ambiguous 还是 compositional 类型的问题?
    2. extracted_entities:查询里的关键名词、概念或标识符是什么?
    3. complexity_estimate:从 0.0(很简单)到 1.0(非常复杂)。
       考虑:需要几步、是否要把多个来源的信息拼起来、
       查询本身是否自相矛盾或有歧义。
    4. ambiguities:列出查询里任何可能有多种解读的地方。

    对复杂度要诚实。一个看起来简单但需要交叉引用的问题,
    至少是 0.5。一个包含「但是」「然而」「已经试过」的问题,
    很可能是自相矛盾的,至少应该给 0.6。

    你不回答用户的问题。你只分析它。""",
    output_type=TaskRepresentation,
)
最后那句话才是重点

「你不回答用户的问题。你只分析它。」——这一句是整个架构的关键。

模型的天性是看到问题就想答。要把它当成一个「零件」用, 你必须明确禁止它干别的活。七个模块的提示词里都有类似的一句。

另一处值得偷的写法:「对复杂度要诚实」,还给了具体锚点 (「看起来简单但要交叉引用 → 至少 0.5」)。光说「评估复杂度」模型会乱给, 给了锚点它才有标尺。这和第七章 grounding 那节说的是同一件事: 模糊的指令换不来可靠的判断。

为什么非要拆成七个 agent

七次模型调用,比一个 agent 一次搞定贵得多。图什么?

图的是「每一步的判断都变成了可检查的数据」。 complexity_estimate=0.7confidence=0.4—— 这些数字能被 if 语句读。而一个大而全的 agent 在内部想了什么,你只能看它最后吐的那段话。

这正是后面 10–12 元认知能存在的前提: 你没法给一段自然语言设阈值,但你能给 0.4 设。

第二部分 · 组装

08 和 09:复制粘贴的巨兽

08_cognitive_loop.py(488 行)和 09_cognitive_agent.py(775 行)不引入任何新概念—— 它们是把前面的文件整段抄进来

抄法是这样的:

01–07(七个模块,各自 39–101 行)
    ↓ 整段复制
08_cognitive_loop.py(488 行)= 01–07 + 循环
    ↓ 整段复制
09_cognitive_agent.py(775 行)= 08 + 10–12 + MCP 接线
    ↑ 这个是唯一号称完整的成品,而它从没被跑过

这不是偷懒,是这个仓库刻意的设计——每个清单要能在书里单独读懂 (前九章全是这个套路)。但代价在这一章格外明显:同一个 CognitiveWorkspace 类,在 12 个文件里出现了好几遍, 改一处得改多处。别试图重构它,那是在跟这本书的排版对抗。

09 里那个不存在的包

09 接了两个 MCP server:

@modelcontextprotocol/server-memory     ← 存在,第六章用过
@anthropic/brave-search-mcp            ← 不存在,npm 404

要跑 09,得把第二个换成 @modelcontextprotocol/server-brave-search—— 和第九章同一个修法。另外它用 os.environ.get("BRAVE_API_KEY", "")缺 key 时静默传空串,搜索会一直失败却不报错。

第三部分 · 元认知

让它知道自己不知道

这三个文件加起来才 319 行,却是全章最有价值的部分—— 而且它们是作者真正跑过的三个。

10 · 信心闸门:不够确定就别说

对应清单 10.10 · 作者验证过 ✓

def check_confidence_gate(workspace: CognitiveWorkspace) -> GateDecision:
    # 硬阈值:低于 0.3 绝不呈现给用户
    if workspace.confidence < 0.3:
        return GateDecision.SIGNAL_UNCERTAINTY

    # 软阈值:0.3 到 0.6 之间,先试着多收集一点
    if workspace.confidence < 0.6:
        # 但只有还有迭代预算时才去收集
        if workspace.iteration_count < 3:
            return GateDecision.GATHER_MORE
        return GateDecision.SIGNAL_UNCERTAINTY

    # 信心够了,但如果发现了矛盾,还是要再查
    if workspace.active_signal == AttentionSignal.CONTRADICTION:
        return GateDecision.GATHER_MORE

    # 信心够了,但如果它在连续下滑 —— 这是个危险信号
    if len(workspace.confidence_trend) >= 3:
        recent = workspace.confidence_trend[-3:]
        if all(recent[i] > recent[i + 1] for i in range(2)):
            return GateDecision.GATHER_MORE

    return GateDecision.PRESENT
最后那一段是这个函数最聪明的地方

前面几个判断都是「信心够不够高」。最后那段问的是完全不同的问题: 信心在往哪走。

[0.9, 0.7, 0.65]——当前值 0.65 过了 0.6 的线, 按前面的规则该直接呈现了。但它连续三次下滑, 说明越查越心虚——很可能马上要发现自己错了。所以拦下来再查。

这是全章我最想让你记住的一个想法「趋势」比「当前值」信息量大。而这也解释了黑板上为什么要存 confidence_trend 这个列表——没有历史,就看不见趋势。

但这些数字是从哪来的?

0.30.63 次迭代—— 全是硬编码的魔数,书里没有给出任何依据。不是实验测出来的, 也没有说明为什么是 0.3 而不是 0.35。

更根本的问题:confidence 这个数本身是模型自己报的。 而模型报的置信度是出了名的不准——它经常对错误答案信心满满。 所以这道闸门的本质是:拿一个不可靠的自评数字,去卡一个精心设计的阈值。

这不是说它没用——「连续下滑就再查一轮」这个思路是对的, 而且很值钱。但那几个具体数字你必须自己在自己的任务上重新标定, 别照抄。

11 · 停滞检测:抓「复读机」失败模式

对应清单 10.11 · 作者验证过 ✓

def detect_stagnation(workspace: CognitiveWorkspace) -> bool:
    # 至少得有两条发现才能比较
    if len(workspace.findings) < 2:
        return False

    # 信号一:最近两条发现的内容高度重合
    last = workspace.findings[-1].content.lower().split()
    prev = workspace.findings[-2].content.lower().split()
    overlap = len(set(last) & set(prev)) / max(len(set(last)), 1)

    if overlap > 0.7:
        # 查来查去查回了同一个东西 —— 卡住了
        return True

    # 信号二:信心连续三步几乎没动
    if len(workspace.confidence_trend) >= 3:
        recent = workspace.confidence_trend[-3:]
        spread = max(recent) - min(recent)
        # 三步之内信心波动小于 0.05
        if spread < 0.05:
            return True

    return False
这个「卡住」的定义很妙

怎么知道智能体卡住了?不看它说什么,看它有没有在动。

信号一:这轮查到的和上轮查到的词重合度超过 70%—— 它在原地打转,反复捞同一批内容。
信号二:信心三步之内几乎不动(波动 < 0.05)—— 既没变好也没变坏,说明新信息没带来任何增量。

回想第九章那个「循环悄悄提前收工」的陷阱——这里是它的反面: 循环停不下来,一直烧钱却没有进展。这两种失败合起来, 才是自主循环的完整风险图。

用词重合度判断「说的是不是一回事」,靠不住

set(last) & set(prev)把两句话打散成词、算交集比例。 这就是第五章 ToT、第七章判分器那个老毛病的又一次现身: 拿字符串操作去理解语义

「电池能量密度提升了 20%」和「能量密度的提升幅度是 20%」—— 意思完全一样,但词几乎不重合,检测不出停滞。 反过来,两段用词接近但结论相反的文本,会被误判成停滞。

真要做,这里应该用第六章的 embedding 算语义相似度—— 而讽刺的是,那正好是同一本书第六章教过的东西。

12 · 知识边界:知道自己不知道

对应清单 10.12 · 作者验证过 ✓

12_boundary_awareness.py 做的事是:综合几个信号, 判断「这事到底在不在我的能力范围内」——有没有查到东西、 记忆里有没有、结果的相关性分数高不高,加权算出一个边界评估。

为什么这个能力比前面所有章都重要

前九章的智能体永远会给你一个答案。查不到?编一个。 工具坏了?绕过去猜。它们没有「我不知道」这个输出。

而在真实场景里,一个说「我不确定」的智能体, 比一个自信地给出错误答案的智能体有用一百倍。 第七章的 grounding 防的是「夹带私货」,这一章防的是更根本的东西: 在不该开口的时候开口。

这就是这一章标题里「认知」的真正含义—— 不是让它更聪明,是让它对自己的状态有感知

这一章值多少

该怎么看待它

一个诚实的评价

这一章学术味最重、代码质量最差:旗舰文件没跑过、 引用的包不存在、阈值全是拍脑袋的魔数、 语义比较用的是词交集。你不能拿它当框架用。

但它的三个想法是真金,而且在别处很少见:

① 趋势比当前值重要confidence_trend 是列表不是数)。
② 记住什么行不通failed_approaches)——前九章都不记。
③ 「我不知道」是个合法输出——这是全书最缺的能力。

建议这么读10/11/12 三个文件加起来 319 行,作者真跑过,思想密度最高,值得逐行看09 那 775 行当架构图纸翻一翻就行—— 它演示的是「可以这么组织」,不是「这样就能跑」。

跟着书走之前,先看这几条

  • 旗舰文件从没被运行过

    作者的 .claude/settings.local.json 显示: 10/11/12 真跑过, 而 08(488 行)和 09(775 行) 只做了 ast.parse 语法检查。 第二重证据:09 引用的 npm 包不存在,跑一次几秒就会撞 404。

  • 09 用了不存在的 npm 包

    @anthropic/brave-search-mcp —— npm 404, @anthropic scope 下没有任何公开包(和第九章同一个问题)。 换成 @modelcontextprotocol/server-brave-search

  • 必须 cd chapter_10

    04_execution_module.py"./docs" 相对路径。 第六章要求在仓库根目录跑,这一章要求 cd 进来——两章互相冲突。

  • 01–08 跑不出任何东西

    八个文件全是类和 agent 定义,没有 __main__只有 09/10/11/12 能独立跑,而 09 因为上面那个包跑不起来—— 实际能跑的只有 10/11/12。

  • 阈值全是魔数

    0.30.60.7 重合度、 0.05 波动、3 次迭代——书里没给任何依据。 而且 confidence 本身是模型自评的、出了名的不准。 思路可以抄,数字必须自己标定。

  • 停滞检测用词交集

    set(last) & set(prev) 算词重合度——同义不同词就检测不出。 又是「拿字符串操作理解语义」(第五章 ToT、第七章判分器都栽在这儿)。 该用第六章的 embedding。

  • 同一个类抄了好几遍

    CognitiveWorkspaceFindingAttentionSignal 在多个文件里重复定义。 这是刻意的(每个清单要能单独读懂),别去重构。

  • BRAVE_API_KEY 静默失败

    09os.environ.get("BRAVE_API_KEY", ""), 缺 key 时传空串、不报错,搜索一直失败却查不出原因。 (和第九章 07_task_loop.py 同款。)