Agent Engineering 课程阅读
首页/课程十 · 常驻主动式 Agent/信源与变化检测:世界变了什么

在 GitHub 查看原文

本页目录

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.py Part 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) .envENABLE_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 的结构化结果,快照原封不动,恢复后跟最后一次看清的世界对比,不会把故障期误报成全部消失又全部新增。把没看到报成没变,是常驻系统最危险的静默谎言。」