Chapter 1 学习笔记:AI Agent 简介
1. 什么是 AI Agent
1.1 分界线在哪
判断一个系统是"对话式 AI"还是"Agent",不看它用了多大的模型,看交互结构:
| 对话式 AI | AI Agent | |
|---|---|---|
| 交互形态 | 你问一句,它答一句 | 你给目标,它自己规划并执行 |
| 能否动手 | 不能,只能生成文字 | 能,通过工具读写文件、调 API、执行命令 |
| 能否纠错 | 不能,答完即结束 | 能,拿到工具结果后调整策略重试 |
| 终止条件 | 生成完毕 | 任务完成 / 迭代预算耗尽 |
书里给的判据更精炼:自主规划执行步骤、调用工具完成任务、根据结果调整策略——三者齐备才算 Agent。
1.2 两个抽象层次(图 1-1)
外层:Agent ↔ Environment 闭环。环境(文件、数据库、网页、用户、其他 Agent)不断返回观察,Agent 选择行动,行动改变环境,产生新观察。这是理解所有 Agent 交互的最小结构。
内层:Model–Harness。这条边界是本章最有工程价值的一刀:
Model= 决策(下一步做什么)
Harness= Agent 边界内环绕模型的运行与治理层:构造上下文、暴露工具接口、维护循环与状态、施加权限/验证/纠正
"Harness"原意是马具——不是限制马奔跑,而是把力量引导到正确方向。模型是那匹强大但不可预测的马。
这句话解释了一个反直觉的现象:模型越强,Harness 越关键。模型自主决策空间越大,出错影响面越大,越需要精细的约束与验证。
2. 核心框架:Agent = LLM + Context + Tools
2.1 三层拆解
| 直觉 | 组件 | 学术概念 | 干什么 |
|---|---|---|---|
| 🧠 大脑 | LLM | 策略(Policy) | 理解意图、思考规划、决定"下一步做什么" |
| 👁 眼睛 | 上下文 | 观察与历史 | 把环境返回的信息组织成当前决策所需的形式 |
| 🦵 手脚 | 工具 | 观察/行动接口 | 规定能读哪些观察、能发出哪些行动、用什么格式 |
公式里的加号是工程组件组合,不是数学等式;且它只描述 Agent 边界之内,不包含 Environment。
2.2 上下文的五个组成部分
每次调用 LLM,上下文由这五块拼成:
┌─ 静态前缀(对话中保持不变)────────────┐ │ ① 系统提示词 System Prompt │ ← 岗位说明书 + 用户记忆 + 环境状态 │ ② 工具定义 Tool Definitions │ ← 能调什么、参数是什么 ├─ 轨迹(随交互不断增长)────────────────┤ │ ③ 用户消息 User Messages │ ← 含 RAG 动态注入的外部知识 │ ④ 模型回复 Assistant Messages │ ← reasoning + content + tool_calls │ ⑤ 工具结果 Tool Results │ ← 框架执行工具后的真实返回 └───────────────────────────────────────┘
Agent 的上下文 = 静态前缀 + 轨迹。这行等式是理解实验 1-1 的钥匙。
2.3 ReAct 循环
把三者串起来的机制:
trajectory = [user_request] repeat: context = stable_prefix + trajectory decision = Model(context) # 思考:下一步做什么 trajectory.append(decision) if decision has tool_calls: observation = execute(tools) # 行动 trajectory.append(observation) # 观察 until 终止响应 or 迭代上限
"想 → 做 → 看 → 想 → 做 → 看"循环往复。注意ReAct 名字里只有 Reasoning + Acting,实际是三个环节——第三个"观察"常被忽略,而它恰恰是实验证明最不能省的那个。
2.4 最重要的一条工程结论
在模型固定时,提升 Agent 表现最主要的手段,往往就是重新定义或扩展观察空间与动作空间。
书里用 Manus 佐证:它并非靠换更强的模型成为通用 Agent,而是取了 Deep Research(搜索)、Coding(代码)、Computer Use(电脑操控)三类 Agent 观察空间与动作空间的并集。
很多看似需要"更聪明模型"的问题,其实只是接口问题。
3. 实验环境搭建(本次实操记录)
3.1 最终结构
D:\WorkStation\Bigdatadevelop\Agent\SSDAgent\ ├── ai-agent-book\ # 仓库(chapter1~10 全量检出,仅 ch4 两个超长文件名建不了) │ └── chapter1\context\ # 本实验目录:agent.py / config.py / main.py / run_experiment_1_1.py │ └── .env # Key 配置(被 .gitignore 的 `.*` 规则忽略) ├── .venv\ # 本地自包含虚拟环境(Python 3.13.12) └── ch1-lab\ # 我自己加的实验脚手架 ├── mock_llm_server.py # 本地 OpenAI 兼容替身 LLM ├── run_ablation.py # 五组消融对照运行器 └── ablation_mock.json # 实测结果
3.2 依赖
python -m venv .venv .venv/Scripts/python.exe -m pip install -r ai-agent-book/chapter1/context/requirements.txt
装到的版本:openai 3.29.0 / pypdf 6.20.1 / numpy 2.5.4 / matplotlib 3.11.2 / tabulate 0.10.0 / reportlab 5.0.1 / python-dotenv 1.2.4 / pytest 9.1.1
3.3 提供商配置
提供商解析集中在agentbook/providers/registry.py一张表里——加一个提供商 = 加一条记录,各章 CLI 的--provider选项自动可选。这是仓库里很值得抄的设计。
| provider | 默认模型 | Key 环境变量 |
|---|---|---|
deepseek | deepseek-v4-flash | DEEPSEEK_API_KEY(本次选定) |
dashscope/qwen/bailian | qwen3.7-plus | DASHSCOPE_API_KEY |
kimi/moonshot | kimi-k3 | MOONSHOT_API_KEY |
doubao/ark | doubao-seed-1-6-thinking-250715 | ARK_API_KEY |
siliconflow | Qwen/Qwen3.5-397B-A17B | SILICONFLOW_API_KEY |
zhipu | glm-5.2 | ZHIPU_API_KEY |
openrouter | 自动映射 | OPENROUTER_API_KEY(主 Key 缺失时兜底) |
ollama | qwen3:8b | 无需 Key,可用OLLAMA_BASE_URL指向本地服务 |
3.4 踩过的坑
| 现象 | 根因 | 解法 |
|---|---|---|
git clone中断,只剩 chapter1/10 | 进程被 SIGTERM,checkout 没做完,留下index.lock和半个索引 | 不删目录重来,而是rm -f .git/index.lock && git reset --hard HEAD(对象包已完整,只需补 checkout) |
rm -rf被拦截 | 安全守卫:2672 个文件 > 50 阈值,需批量删除确认 | 改走原地修复路径 |
Filename too long | Windows MAX_PATH,chapter4 的两个校验产物路径过长 | 不影响第一章,忽略 |
| Mock 服务返回 404 | 本机有 HTTP 代理,httpx 以 absolute-form 发请求,self.path变成完整 URL | Mock 端做路径归一化 + 客户端设NO_PROXY=127.0.0.1,localhost |
NameError: name 'openai' is not defined | agent.py只写了from openai import OpenAI,异常分支却用openai.APITimeoutError——只在出错时才炸的潜伏 bug | 运行器里补_agent_mod.openai = _openai,不改仓库代码 |
最后一条正好印证本章主题:Harness 里的错误处理代码,平时看不见,一旦真出错反而自己先崩了。
4. 实验 1-1:上下文的关键作用
4.1 设计
消融实验(Ablation Study)= 一次只移除一个组件,与完整版比较。系统提示词不参与消融(没有它连角色认知都没有,测了没意义),其余四个组件各做一组:
| 组 | 移除了什么 | 在代码里怎么落地 |
|---|---|---|
full | 无(基线) | 发完整轨迹 |
no_history | 历史消息 | _prepare_messages_for_api()只发 system + 当前任务 |
no_reasoning | 思考过程 | 写回轨迹前剥离每条 assistant 的reasoning_content |
no_tool_calls | 工具定义 | 请求里直接省略tools参数 |
no_tool_results | 工具执行结果 | tool 消息必须存在(协议要求),但内容置空 |
4.2 实测结果
任务:请调用计算器计算 (125 + 375) * 8,并给出结果。(正确答案 4000),迭代上限 5。
| 组 | 迭代 | 工具行动 | 重复行动 | 终止 | 最终答案 |
|---|---|---|---|---|---|
full | 2 | 1 | 否 | ✅ | 4000(来自工具观测) |
no_history | 5(触顶) | 5 | 是 | ❌ | 无 |
no_reasoning | 2 | 1 | 否 | ✅ | 4000(测不出退化) |
no_tool_calls | 1 | 0 | 否 | ✅ | 4000,但自述"由我自行估算,未经工具观测" |
no_tool_results | 5(触顶) | 5 | 是 | ❌ | 无 |
⚠️诚实标注:以上为本地 Mock 后端跑出的结果,用于验证实验链路(ReAct 循环、四个工具的真实执行、五个消融开关、结果落盘)确实通畅,并复现了教材预期的行为形态。Mock 不模拟智能——它的决策规则就是"看得到什么就做什么",而这一点恰好是实验要考察的对象。换成真实模型(DeepSeek)后数值会变,需以真实运行为准。
链路验证的旁证:
仓库自带回归测试
pytest .→52 passed官方 CLI
main.py --mode single --provider ollama --context-mode full→ 正常输出Final Answer: 4000,Iterations: 2 / Tool Calls: 1工具真实执行:
calculate走calc_sandbox.safe_eval(AST 白名单,堵住了().__class__这类逃逸)、convert_currency走固定汇率表
4.3 怎么读结果:completed≠ 做对
这是本章我最想记住的一点。框架给了三个层次的判据:
| 字段 | 含义 |
|---|---|
completed | 循环返回了终止响应(不代表任务做对了) |
task_success | 数值评分标准下的正确性 |
outcome | correct/no_unsupported_numbers/unsupported_numbers/incorrect/no_terminal_response |
unsupported_numbers(编造数字)是"完成率"那一列永远显示不出来的失败,也是生产中最要命的一种:用记忆中的汇率拼出来的答案,排版和用工具结果算出来的答案一模一样。
no_tool_calls组就是活例子——它 1 轮就给出了格式工整的答案 4000,数字也对,但没有任何观测支撑。如果只看"是否终止"和"答案对不对",这一组会被判为满分。
可依据性(groundedness)与正确性(correctness)是两条独立的轴。没有观测却报出正确总数的模型,同样不是"读到"的。
4.4 三条结论
上下文决定了 Agent 能看到什么,而它只能基于看到的信息决策。没进入上下文的信息,对模型来说等于不存在。
组件并不等价——衡量标准是"它承载的信息能否从别处重建"。
工具结果(
no_tool_results)→ 无法重建 →最致命,缺失即盲目执行、反复重试历史消息(
no_history)→ 无法重建 → 重复劳动、触顶工具定义(
no_tool_calls)→ 无法重建 → 彻底失去行动能力(但会照样作答!)思考过程(
no_reasoning)→能从工具结果重建→ 代价极小,本次实测确实没有退化
"给出了回答"不等于"完成了任务"。上下文残缺时典型的失败不是报错退出,而是一个看上去毫无破绽的答案。
4.5 教材里两个值得记住的细节
提示词约束只是概率偏移,不是开关。加了"不得自行估计汇率"后,同一模型在带约束任务下仍有 2/13 组报出无依据数字。别依赖提示词,要靠可依据性检查(grounding.py)。
"静默隐藏"和"可见占位符"不是同一个消融。
[Tool result hidden]这个占位符本身就是一个信号——模型能看出"有观测被藏了"从而停下来说明情况;静默置空则 7 次里 6 次跑到迭代上限。拿走一个信号 ≠ 拿走观测。
5. 【扩展】拆解一个熟悉的 Agent 产品
选WorkBuddy——每天都在用,且能拿到真实结构证据,不靠猜。
5.1 LLM(大脑)
宿主按会话调度选择模型档位(可切到 lite / reasoning 等不同档)。它只负责"下一步做什么";它不持久化任何东西——所有跨会话的连续性都由下面的 Harness 层负责。
5.2 Context(眼睛)——四层拼装,正好对应本章的五个组件
| 层 | 对应书中组件 | 内容 |
|---|---|---|
| 静态前缀 | ① 系统提示词 | 角色与行事风格(SOUL.md)、身份(IDENTITY.md)、用户画像(USER.md) |
| 记忆 L1 | ① + 检索 | 云端长期画像(自动注入)+conversation_search跨会话检索历史对话 |
| 记忆 L2 | ① | ~/.workbuddy/MEMORY.md——跨项目的显式硬规则 |
| 记忆 L3 | ① | 工作区.workbuddy/memory/YYYY-MM-DD.md(日志)+MEMORY.md(项目长期笔记) |
| 轨迹 | ③④⑤ | 本轮对话 + 工具调用请求 +工具执行结果回填 |
有意思的对照:L1 是"系统自动学到的",L2 是"用户明确写死的",L3 是"项目内的活日志"。三层解决的是同一个问题的不同时间尺度——和书里 1.4 节"上下文适应 / 外部产物 / 参数更新"三条更新路径是同构的。
5.3 Tools(手脚)——五类齐全,正好对应第四章的分类
| 类别 | 实例 |
|---|---|
| 感知 | ReadGrepGlobWebFetchWebSearchBash(读) |
| 执行 | WriteEditBashPowerShellPython |
| 协作 | Agent(子代理:general-purpose / Explore / Plan)、SendMessage(多 Agent 协同) |
| 用户沟通 | present_files(产物卡片/HTML 预览)、show_widget(内联可视化)、AskUserQuestion |
| 事件触发 | automation_update——定时/一次性任务,外部时间事件驱动 Agent |
| 动态扩展 | Skill(按需加载专家能力包)、Connector/MCP(接入外部服务与数据源) |
5.4 Harness(约束层)——本次会话的三个活证据
书里说 Harness 负责"权限、验证与纠正",今天被它拦了三次,全都是正面例子:
safe-delete 守卫:我
rm -rf一个 2672 文件的目录时被拦截并强制确认——防止破坏性误操作。路径/命令管控:
cmd /c dir被拒绝,要求用原生语法;禁止绑定0.0.0.0起服务。personal_files_safety:对个人目录的扫描默认只读,要求"先列清单再确认"。
这就是 Harness 的价值:模型想做、也能做到,但 Harness 让它先停下来被审视一遍。和书里"马具不是限制奔跑,而是引导方向"完全对上。
5.5 反向对照:WeijuPetOS
把这套框架套回自己的项目,立刻能看出差距在哪:
| 三层 | WorkBuddy | 微橘宠物系统(现状 → 可 Agent 化方向) |
|---|---|---|
| LLM | 宿主调度,决策内核 | 尚未接入;可先做"店长问答"这类低风险入口 |
| Context | 四层记忆 + 实时轨迹 | 有数据(订单/库存/会员),但未组织成模型可消费的上下文——缺 RAG 检索层与用户记忆 |
| Tools | 五类齐全 + 动态 Skill | 后端 API 就是天然工具集,缺的是工具定义(OpenAI function schema)与权限分级 |
| Harness | 权限/守卫/确认 | 缺迭代上限、缺可观测轨迹、缺可依据性检查 |
按本章结论,优先级很清楚:先把"观察空间和动作空间"定义出来(Context + Tools),再谈换更强的模型。微橘现在的瓶颈是接口问题,不是模型问题。