本页目录
Lesson 02 — 信源与变化检测:世界变了什么
本课目标:让 Agent 醒来后第一件事不是研究,而是问「和上次比,变了什么」——item 级内容哈希 + 快照 diff,五毛钱的机械层挡住最贵的那一步(全量研究)。并立下常驻系统的诚实纪律:「没能看到」≠「没有变化」。
学完你能回答面试官那句:「怎么避免你的常驻 Agent 每天重复研究同一主题?」
0. 起点:L00 基线的第②环
基线 Day2:世界纹丝没动(内容相似度 94%),现状仍全量重研烧了 1053 token、再推一份全量报告。更尴尬的是 Day5:信源打不开,现状 web_search 的兜底把「搜索失败」字符串混进材料——「没能看到」被写成了「今天的情况」。
本课在研究图前面加一层感知:
调度器叫醒(L01)
│
▼
┌─────────────────────────────┐
│ watcher.scan_source() │ ①fetch 信源 → ②与快照 diff → ③ChangeSet
│ (机械层,无 LLM,≈免费) │
└──────────┬──────────────────┘
│
┌───────┼──────────────────┬─────────────────┐
▼ ▼ ▼ ▼
建仓 确认无变化 变化集非空 ok=False(没能看到)
(first) (到此为止, (→ L03 增量研究) (→ 显式呈现,L07 进日报;
全量研究 不进研究图, 绝不冒充「无变化」)
合理) 不花钱)
1. 适配器抽象:信源是什么不重要
scan_source(source_id, fetch) 的 fetch 是任何返回条目列表的 callable,条目走鸭子类型(有 item_id/title/content 属性即可):
- 5 日时间线(
eval_agent/ambient_timeline.py):课程贯穿信源,SourceItem直接可用; - 本地目录适配器(
make_dir_fetch,内置):每个 md/txt 文件一条目——离线可用的真实信源(盯本地笔记/导出文件); - 搜索适配器(ddgs 包一层,练习实现):需联网,标注可选。
🎯 核心认知:watcher 不关心信源长什么样,只关心「上次的指纹 vs 这次的指纹」。加一种信源 = 写一个 fetch 函数,检测逻辑零改动。
2. 快照与 diff:粒度对了,增量是免费的
2.1 规范化 + 内容指纹
def normalize(text): return "".join(text.split()) # 压掉全部空白
def content_hash(item): return md5(normalize(title+content))[:16]
L00 已经用数字证明了全文 diff 的不可靠:Day2 条目顺序一打乱,文本相似度掉到 49%——看起来变了一半,其实什么都没变。code.py Part 1 的对照实验:
Day1→Day2(世界没变): 全文 diff:相似度 46% → 误报「变了 54%」
item 哈希:新增[] 变更[] → 零变化(正确)
Day2→Day3(多了一条): 全文 diff:相似度 92% → 只知道「变了一点」
item 哈希:新增['item-e'] → 精确指认
🎯 核心认知:变化检测的正确粒度是 item 级。粒度对了,「哪一条新增/变更/消失」是免费得到的——这正是 L03 增量研究需要的输入。全文 diff 连「变没变」都答不可靠,更别提「变在哪」。
2.2 快照表与三类变化
sqlite snapshots(source_id, item_id, content_hash, first_seen_at, last_seen_at, last_changed_at):
- new:item_id 不在快照 → 新条目;
- changed:在快照但指纹不同 → 内容变更(Day4 的 item-c 反转走这条);
- gone:快照有、这次没有 → 从快照删除(只报一次;再出现按 new 报——不删的话每次扫描都重复报同一批 gone,永不收敛)。
与 ops-L10 语义缓存的亲缘与区别:都是内容指纹去重;那边对象是问题(同义问法命中缓存,省一次回答),这边对象是信源内容(识别「没有新东西」,省一轮研究)。
3. 两条纪律(本课灵魂)
3.1 「没有变化」是一等公民结果
ChangeSet.is_no_change() 为真 → 本轮到此为止:不拆题、不检索、不写报告、不打扰。Day2 的浪费从 1053 token 归零。「今天没事」不是异常路径,是常驻 Agent 最常见的正常结局——大多数日子世界就是没变。
3.2 「没能看到」≠「没有变化」
fetch 抛任何异常 → ChangeSet(ok=False, error=...),且快照不动:
def is_no_change(self):
return self.ok and not self.has_changes() # ← failed 没资格说「确认无变化」
- 为什么快照不动:故障期若清了快照,恢复后会把整个世界误报成「全部消失又全部新增」;快照留在「最后一次看清的样子」,恢复扫描无误报(
code.pyPart 2 的「恢复」行验证了这一点)。 - 为什么是异常而不是错误字符串:字符串可以被下游「假装没看见」(L00 基线里它就混进了报告);
ok=False的结构化结果强迫每个消费方显式分支。这是 agent-ops L03「诚实降级协议」(工具返回 ok/degraded/failed 而非兜底文本)在常驻场景的直系延伸——那里管一次工具调用的诚实,这里管一天的扫描结果的诚实。
💡 把「没看到」报成「没变」是常驻系统最危险的静默谎言:用户以为世界安静,其实是 Agent 瞎了。L07 的日报和缺勤心跳会把这条纪律延伸到服务级(「Agent 还活着吗」)。
4. 流派对比:怎么知道「有没有新东西」?
| 流派 | 做法 | 取舍 |
|---|---|---|
| ① 全量重扫 + 全文 diff | 每次拉全文,与上次比相似度 | ✅ 实现 10 行;🚫 顺序/空白全是误报源(L00 实测 49%),且答不出「变在哪」 |
| ② item 级哈希 + 快照(本课) | 条目指纹入库,逐条对比 | ✅ 免费、确定性、精确到条、误报只剩「真实改写」;🚫 需要信源可拆条目(大多数可以:文章/commit/公告天然是条目) |
③ 增量游标 / 订阅推送(RSS、webhook、If-Modified-Since) |
信源自己告诉你「从上次之后的新内容」 | ✅ 最省(不用重扫);🚫 需要信源配合——盯梢场景恰恰常盯「不配合的信源」;有推送时优先用,快照法做兜底 |
| ④ LLM 判断变化 | 把两版内容给 LLM 问「变了什么」 | ✅ 能理解语义(改写≠变化);🚫 贵、慢、不确定——不该用在「每次醒来都要跑」的机械层 |
选 ② 的理由 + 分层设计:机械层(本课)用 ②——便宜、每轮必跑、诚实上报「哪条动了」;语义层(L04)用 ④ 的思路——只对机械层报上来的变化判「算不算实质、值不值得说」。便宜的先跑,贵的兜底,与 docint 课「本地 OCR 先跑、VLM 兜底」同一思维。
5. 跑起来
cd ambient-agent-lessons/02_source_watch
python code.py # 零 API、零联网、零等待
Part 2 关键输出:
Day2: [timeline] ✅ 确认无变化
Day3: [timeline] 🔔 变化:新增1 变更0 消失0
Day4: [timeline] 🔔 变化:新增1 变更1 消失0
Day5: [timeline] ❌ 没能看到(SourceUnavailableError…)——不等于没有变化
快照条数:6(故障期原封不动)
恢复: [timeline] ✅ 确认无变化 ← 无误报
6. 落地清单
| 文件 | 改动 | 如何验证 |
|---|---|---|
src/research_assistant/watcher.py |
新增:normalize/content_hash/ChangeSet/scan_source(快照 diff + 诚实 failed)+ make_dir_fetch 目录适配器 |
见下 |
src/research_assistant/config.py |
新增:enable_source_watch(默认关;daemon 接线在 L06) |
.env 设 ENABLE_SOURCE_WATCH=true |
tests/test_watcher.py |
新增:13 个测试(5 日逐日语义/failed 纪律/快照保全/gone 一次性/目录适配器) | pytest tests/test_watcher.py -q |
研究图 / service 零改动——watcher 是图外的感知层,不开不碰任何现有路径。
验收
cd portfolio-projects/research-assistant
python -m pytest -q # 232 + 13 = 245 passed
python -m pytest tests/test_watcher.py -q # 13 passed
7. 本课在两条主线上的位置
- 倒置主线:第②环——「研究什么」不再由人指定全量主题,而由变化集决定(建仓全量、之后只看动过的条目;L03 把变化集接进研究图)。
- 注意力经济主线:机械层是成本挡板——「确认无变化」的日子研究费直接归零(基线 Day2 的 1053 token);同时它替注意力把好第一道关:连变化都没有的日子,根本轮不到「要不要打扰」的问题。
🎯 面试话术
「我的常驻 Agent 醒来先做机械层变化检测:item 级内容哈希 + 快照 diff——五毛钱成本回答『有没有新东西』,没有就不花研究的钱。为什么不用全文 diff?我实测过:条目顺序一打乱,文本相似度从 94% 掉到 49%,全是误报;粒度到 item 级之后,『哪一条变了』是免费得到的,直接喂给增量研究。
两条纪律:一,『没有变化』是一等公民结果,空变化集就不进研究图;二,『没能看到』不等于『没有变化』——信源故障返回 ok=False 的结构化结果,快照原封不动,恢复后跟最后一次看清的世界对比,不会把故障期误报成全部消失又全部新增。把没看到报成没变,是常驻系统最危险的静默谎言。」