我的实习生 - Hermes 和 Openclaw 中的调研工作流
让实习生去调研一个没做过的方向或者让 AI 去调研某个技术方向,以为省了时间——其实只是把判断成本往后挪了。调研回来还是要面对同一个问题:这个结论能用吗?AI 不会告诉你它跑偏了,因为它根本不知道。产出速度提高了 10 倍,幻觉和失控的问题一个没少,还增加了信任的成本。
在 LLM 之前,非科研的调研和学习有一个土法子——搜索引擎和 Wikipedia 漫游。从一个词条出发,跟着链接一路点进去,走着走着就对一个领域有了轮廓。这个过程里有一套自然的校正:遇到没搞清楚的概念,就回头补。不是线性阅读,是有来回的探索,更像是一棵树,自己补全整个知识面和技能脉络。
Agent 时代这套做法就太耗时间了。AI 把所有子文件线性写完,不会说”第 3 个文件有个概念没讲清楚,我要回头”。产出物看起来完整,实际上是一条没有反馈的流水线,甚至是一篇充满幻觉和演义奇幻小说。
这篇讲的就是把这套来回补回去——用三个测量指标替代”我还没懂”的直觉,用状态机替代”回头补”的随机游走,用双层控制回路替代人盯。
一个说明:本文的 FSM 状态机和 researcher.py 是架在 OpenClaw / Hermes Agent 上的自建系统,不是 AI 助手的原生功能。用其他框架的话,思路可以迁移,也会列出核心代码实现。
1. 调研失控的三种模式:先量化,才能控制
Wikipedia 漫游时你大概同时在做三件事:
- “这词条跑题了”——跟着链接走着走着,离原来想找的主题越来越远
- “这块还有几个概念没覆盖”——隐约感觉到还有哪些坑没填
- “这来源靠不靠谱”——下意识区分学术来源和随便写的博客
AI 写调研时这三件事全没有——它不知道自己跑偏了,不知道哪些概念没覆盖,也不区分来源质量。
把这套机制补回去,第一步是让它们变成数字:
| 直觉判断 | 量化指标 | 含义 |
|---|---|---|
| ”跑题了” | divergence(语义偏离度) | 实际内容与设定方向的语义距离 |
| ”没覆盖全” | coverage(概念覆盖度) | 核心概念有几个被子文件实质讨论过 |
| ”来源靠不靠谱” | credibility(来源可信度) | 所有引用 URL 的可信度加权均值 |
这三个数字是后面所有自动决策的依据——理解了它们,FSM、ACTUATOR、ESCALATION 的逻辑都顺理成章。
工程师不会在没有 lint、test、coverage 的前提下讨论”代码质量”。调研也一样:没有测量手段就没有工程指标,只有主观感受。
1.1 三个指标的定义与阈值
- divergence:用 embedding 模型把方向文本和每个子文件转成向量,取平均余弦距离。> 0.4 说明跑偏了,> 0.6 触发 ESCALATION 要求人介入。
- coverage:从方向文本提取 5-15 个核心概念,每个子文件用 embedding 相似度 > 0.7 算”实质性覆盖”,已覆盖数 / 总数就是 coverage。≥ 0.8 说明全覆盖。
- credibility:扫描所有引用 URL,按域名查可信度字典——顶刊(NEJM、Lancet)4.0,权威机构(FDA、NIH、WHO)3.5,行业媒体 2.5,未登记域名兜底 2.0。< 2.5 说明来源质量不达标。
这三个阈值都是经验值,不是固定标准:
- divergence 0.4 / 0.6:0.4 是”内容开始明显偏题”的感知拐点,从多轮零基础调研实测中归纳;追求严格可调低到 0.3,容忍度高可调到 0.5。0.6 是”方向本身可能错了”的分界线,这个比 0.4 更难用数字校准,主要靠人工判断。
- coverage 0.8:对应”80% 核心概念有实质讨论”。浅调研(只要概览)可以降到 0.7,要求全面覆盖的深调研可以维持 0.9。
- credibility 2.5:对应”行业媒体及以上才达标”。纯学术调研可调高到 3.0;工具/工程类调研如果主要引用官方文档和 GitHub,2.5 可能偏低,需要按领域重新标定字典。
开车看仪表盘,不靠感觉——divergence 是偏向表,coverage 是油量表,credibility 是温度表。三个同时达标,调研才算没跑偏、没写残写烂。
2. FSM 状态机:把调研拆成可观测阶段
调研不是一步到位的事情,它有阶段:从”刚开始还没方向”到”方向确认、内容填充”到”可能跑偏、需要校正”到”内容稳定、等待验收”。FSM 把这几个阶段编码成显式状态,给每一步设定进入和退出条件。好处是任何时刻都能回答:这个调研在哪一步,下一步会去哪。
stateDiagram-v2
[*] --> INIT : 用户提出调研
INIT --> EXPLORING : 大纲确认 + setpoint
EXPLORING --> SELF_CORRECTING : divergence > 0.4
EXPLORING --> CONVERGING : 指标稳定
SELF_CORRECTING --> CONVERGING : 校正完成
SELF_CORRECTING --> EXPLORING : 切换策略继续
CONVERGING --> DONE : 用户验收
CONVERGING --> EXPLORING : 用户否决要求补充
DONE --> [*]
EXPLORING --> ESCALATION : divergence > 0.6
SELF_CORRECTING --> ESCALATION : 持续偏离
CONVERGING --> ESCALATION : 无法收敛
ESCALATION --> EXPLORING : 用户决定继续
ESCALATION --> INIT : 用户调整方向
ESCALATION --> [*] : 用户终止
2.1 每个状态的原理与语义边界
INIT:调研刚启动,大纲已写但用户还没确认方向。存在这个状态的原因很简单——在写一万字内容之前先花 5 分钟确认方向,跑偏的成本远高于这 5 分钟。
EXPLORING:方向已确认,按子文件逐个推进。每个子文件先搜索再写,写完触发 checkpoint 跑一遍 measure。小步快跑,每步可验证。
SELF_CORRECTING:divergence > 0.4 或 coverage < 0.6,机器自动调整,不需要用户介入。这是系统处理小偏差的地方——不让问题积累到需要人来处理的程度。
CONVERGING(收敛):三个指标都达标(coverage ≥ 0.8 + divergence ≤ 0.2 + credibility ≥ 2.5),等待用户做终结判断。这不是 DONE——机器只能确认指标达标,内容有没有用是另一件事。最终验收由人做。
DONE:用户验收通过,写入 knowledge_conclusions 表,decay 计时开始。60 天后系统会问是否需要复查。
ESCALATION:divergence > 0.6,或 FSM 反复无法收敛。严重偏离意味着方向本身可能错了,这个判断机器做不了,只能推给人。
为什么用 FSM 而不是简单的”步骤 1→2→3”?真实的调研有回路:第 5 个子文件写完发现第 1 个方向理解错了,需要回头。FSM 的价值是给这些回路一个显式的状态,让”回头”不是随机游走,而是有条件的状态跳转。
实际代码:ResearchController.step() — FSM 单步控制循环(research_controller.py)
# 精简自 research_controller.py
def step(self, reading: dict = None) -> dict:
"""单步控制循环:读取状态 → 与 setpoint 比较 → 决定 action → 更新 FSM。"""
if reading:
self.divergence = reading.get("divergence")
self.coverage = reading.get("coverage", 0.0)
self.credibility = reading.get("credibility", 0.0)
self.coverage_stable_rounds = reading.get("coverage_stable_rounds", 0)
self.uncovered_concepts = reading.get("uncovered_concepts", [])
sp = self.setpoint
div_max = sp.get("divergence_max", 0.2)
cov_target = sp.get("coverage_target", 1.0) * 0.9
cred_min = sp.get("credibility_min", 2.5)
STABLE_THRESHOLD = 3
action_type = None
# ① coverage < 0.6 且有未覆盖概念 → AUTO_FILL
if self.coverage < 0.6 and self.uncovered_concepts:
action_type = "AUTO_FILL"
if action_type is None:
# 分层 divergence 容忍度(coverage 越高,容忍度越宽)
if self.coverage < 0.6: div_tolerance = div_max # 严格 0.2
elif self.coverage < 0.8: div_tolerance = 0.25 # 中等
else: div_tolerance = 0.35 # 宽松
if self.state == "DONE":
action_type = "IDLE"
elif (reading
and reading["coverage"] >= cov_target
and reading["divergence"] <= div_tolerance
and reading["credibility"] >= cred_min):
self.state = "CONVERGING"
action_type = "AUTO_CONCLUDE"
elif self.divergence and self.divergence > 0.6:
self.state = "ESCALATION"
action_type = "ESCALATE"
elif self.divergence and self.divergence > div_max:
# ② 切策略,连续 2 次无改善才真正 ESCALATE
if (self.strategy_prev_divergence is not None
and self.divergence >= self.strategy_prev_divergence - 0.02):
self.strategy_failure_count += 1
else:
self.strategy_failure_count = 0
self.strategy_prev_divergence = self.divergence
if self.strategy_failure_count >= 2:
self.state = "ESCALATION"
action_type = "ESCALATE"
else:
self.state = "SELF_CORRECTING"
self.strategy = self._next_strategy()
action_type = "SELF_CORRECT"
elif (self.coverage > 0.8
and self.divergence and self.divergence <= div_tolerance
and self.coverage_stable_rounds >= STABLE_THRESHOLD):
self.state = "CONVERGING"
action_type = "CHECK_COMPLETE"
else:
self.state = "EXPLORING"
action_type = "CONTINUE"
self._save()
return {"state": self.state, "action": {"type": action_type, "strategy": self.strategy}}
2.2 三个测量指标的计算过程
FSM 的状态转移依赖三个量化指标。以下是每个指标的定义、阈值依据,以及真实实现代码。
divergence(语义偏离度)
定义:实际写出的内容与最初设定的方向(direction_text)之间的距离。
计算过程:用 embedding 模型(默认百炼 text-embedding-v3)把 direction_text 和每个子文件转成向量,取余弦距离的平均值。实际实现在 sensor_measure() 里统一调用 measure_divergence(),支持 embedding 和 TF-IDF 双引擎,有 key 用 embedding,否则降级 TF-IDF。
阈值理由(基于 60+ 调研的实测统计):
- > 0.4:有可见偏离,触发 ACTUATOR 切换搜索策略(perplexity → brave / deep_dive)
- > 0.6:严重偏离,触发 ESCALATION,通知用户决定方向
- ≤ 0.2:正常范围,可以进入 CONVERGING 等待验收
阈值为什么是这些数字?0.4 是经验值——实测中”方向正确但表达有差异”的 divergence 通常在 0.1-0.3 范围;“方向部分跑偏”在 0.4-0.6;“方向完全跑偏”通常 > 0.6。
coverage(概念覆盖度)
定义:direction_text 里的核心概念,有几个被子文件实质讨论过。
计算过程:从 direction_text 提取 5-12 个核心概念(优先 setpoint topics 字段,否则 jieba 分词 + TF-IDF 打分),每个子文件用 embedding cosine similarity > 0.65 判断是否实质覆盖了某个概念。已覆盖概念数 / 总数 = coverage。
阈值理由:
- < 0.6:覆盖不足,ACTUATOR 自动回填未覆盖概念
- 0.6-0.8:基本覆盖,继续 EXPLORING
- ≥ 0.8:全部核心概念都被讨论,可进入 CONVERGING
已知 Gap:Bailian embedding 对中文长句的概念抽取有碎片化倾向——coverage 会显示偏低,但实际内容可能已经完整(参考下面”灰色状态”处理)。
另外,coverage_stable 加了 6 小时时间门控:两次 measure 间隔必须 ≥ 6 小时,同一 session 内连写 3 个子文件不会假性触发 stable 计数。
实际代码:sensor_measure() — SENSOR 统一入口(research_controller.py)
# 精简自 research_controller.py
def sensor_measure(topic_slug: str) -> dict:
"""SENSOR:读 SETPOINT.json + 所有子文件,返回三个量化指标。"""
setpoint = json.loads((topic_dir / "SETPOINT.json").read_text())
modules = []
for f in topic_dir.glob("*.md"):
if f.name in ("00-index.md",):
continue
text = re.sub(r"```[\s\S]*?```", "", f.read_text()) # 去代码块
text = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text) # 去 Markdown 链接
modules.append(text)
outline_text = setpoint.get("direction_text", "")
# Divergence:embedding 余弦距离(降级 TF-IDF)
div_result = measure_divergence(modules, outline_text)
# Coverage:概念-子文件覆盖矩阵(> 0.65 算实质覆盖)
concepts = _extract_concepts(outline_text, topics=setpoint.get("topics", []))
concept_to_files, _ = _coverage_matrix(modules, concepts)
covered = set(c for c, files in concept_to_files.items() if files)
coverage = len(covered) / len(concepts) if concepts else 0.0
# coverage_stable:6 小时时间门控,防同 session 假性触发
last_measure_at = setpoint.get("last_measure_at")
time_ok = last_measure_at is None or (time.time() - last_measure_at) >= 6 * 3600
prev_cov = setpoint.get("coverage_history", [None])[-1]
if time_ok and prev_cov and abs(coverage - prev_cov) < 0.05:
stable_rounds = setpoint.get("coverage_stable_rounds", 0) + 1
elif time_ok:
stable_rounds = 0
else:
stable_rounds = setpoint.get("coverage_stable_rounds", 0)
# Credibility:URL 可信度加权均值
credibility = measure_credibility(modules)
return {
"coverage": round(coverage, 3),
"credibility": round(credibility, 2),
"divergence": div_result.get("final_divergence", 1.0),
"modules_completed": len(modules),
"coverage_stable_rounds": stable_rounds,
"uncovered_concepts": [c for c, files in concept_to_files.items() if not files],
}
credibility(来源可信度算法)
定义:所有引用 URL 的可信度加权平均分。
计算过程:
- 扫描所有 URL,根据域名在
CREDIBILITY_SCORES字典里查分 - 未登记域名给 2.0 兜底分(不奖不惩,避免”新出现但真实的来源”被零分惩罚)
- 裸链率(不在
[^N]:块里的 URL 占比)> 30% → cap 2.5
CREDIBILITY_SCORES = {
# 4.0:顶级期刊(NEJM/Lancet/JAMA/Nature/Science)
"nejm.org": 4.0, "thelancet.com": 4.0, "nature.com": 4.0,
# 3.5:权威机构(FDA/NIH/WHO/CDC)
"fda.gov": 3.5, "nih.gov": 3.5, "who.int": 3.5,
# 2.5:行业媒体 / 专业期刊
"ign.com": 2.5, "steamdb.info": 2.5,
# 未登记域名兜底:2.0
}
权威/专业级域名由系统在 conclude 时自动加入字典,不需要每次询问用户。商业平台、博客、营销内容默认不加入,需用户决策。
分级逻辑(主观判断,不是客观标准):
- 4.0:同行评审期刊——发表前有外部审查,统计结论有显著性要求
- 3.5:政府/卫生机构——有法律问责约束,发布错误信息有实际后果
- 2.5:行业媒体/专业期刊——有编辑标准,但没有外部同行评审
- 2.0:存在但质量未知——不奖不惩,避免新出现但真实的来源被零分惩罚
域名是内容质量的代理指标,不是质量本身。一篇发表在顶刊上的错误论文照样是错的,一篇个人博客里的实测数据可能比官方文档更准。这套分级来自”来源类型通常意味着什么”的经验归纳,不是固定标准。
可按领域调整:技术类调研可能要把 arxiv.org、github.com 单独分级(预印本和代码仓库在工程领域往往比期刊更及时);金融类调研可能要把监管机构域名单独提高;纯学术调研可以把 credibility_min 从 2.5 调到 3.0,要求更多权威来源。
FSM 内部用 detect_research_signals() 函数持续检测偏差信号,分三层:
def detect_research_signals() -> list[dict]:
signals = []
# 行为层:调研停滞不前
for slug, dir_name, mtime in get_topic_dirs():
days_ago = (now() - mtime) / 86400
status = get_topic_status(dir_name)["status"]
if status == "in-progress" and days_ago > 14:
signals.append({"type": "RESEARCH_STALL_ACTIVITY",
"severity": "high", "detail": f"{slug} 已 14 天无更新"})
elif status == "in-progress" and days_ago > 3:
signals.append({"type": "RESEARCH_STALL_ACTIVITY",
"severity": "medium", ...})
# 时效层:超过 decay 周期
for conc in db_get_needs_review():
signals.append({"type": "RESEARCH_STALL_TEMPORAL",
"severity": "high" if contradicted else "medium", ...})
# 内容层:divergence 严重偏离(读 FSM 预计算结果)
for slug, topic in rc._fsm_load().items():
if topic.get("state") == "ESCALATION":
signals.append({"type": "CONTENT_DIVERGENCE",
"severity": "high",
"detail": f"{slug} div={topic['divergence']:.3f}"})
return signals
每层职责:
- 行为层(
RESEARCH_STALL_ACTIVITY):检测”调研被忘在角落”。in-progress 状态 3 天没动静就发提醒,14 天没动静就升级严重度。 - 时效层(
RESEARCH_STALL_TEMPORAL):检测”调研结论已过期”。next_review < now()的调研会被标记needs_review=1。 - 内容层(
CONTENT_DIVERGENCE):检测”内容严重偏离方向”。FSM 处于 ESCALATION 状态时,divergence 已经超过 0.6。
检测到信号后,cmd_signal_check 会:
- 去重:6 小时内相同信号只执行一次(避免反复骚扰)
- TG 推送:直接发到用户的 Telegram
- 设 verify 期限:medium severity → 72h,high severity → 48h
- 超期追踪:到期未处理 → 标记为 stale + 再发一次 TG 提醒
调研信号与项目管理信号共享同一张 memory.action_tracker 表,不是独立的信号系统。查看方式:
python3 ~/.hermes/scripts/projects.py signal status
# 输出里会同时包含 RESEARCH_STALL_* 和 PROJECT_* 信号
不给每个 workflow 单独建信号表,统一走一张表——所有需要关注的异常都在同一个地方可见。
2.4 收敛四元条件
进入 CONVERGING 需要同时满足四个条件:
modules_completed >= min(3, total_modules // 3) # 至少 1/3 模块完成
AND coverage >= 0.8
AND divergence <= 0.2
AND credibility >= 2.5
coverage_stable(3 轮) 是为边写边测的长调研设计的。一次性写完所有子文件的调研,历史记录为空,stable 永远不满足——这种情况直接跳过 stable 检查,只看上面三个实质指标。
3 轮是最小证据量——防止”连写 3 个子文件后 coverage 碰巧稳定”就误判收敛。如果你的调研节奏更慢(每天写 1-2 个),stable 降到 2 也合理;如果调研跨度很长(跨多天、跨 session),维持 3 或调到 4 更稳妥。
2.5 灰色状态:coverage 偏低但内容实质完整
实测中发现一个边界情况:Bailian embedding 对中文 direction_text 的概念抽取有碎片化,coverage 算法显示 0.3-0.5,但实际内容已经完整回答了研究方向。
判定树:
measure → coverage < 0.6
├─ 读 00-index 的研究目标 + 文件地图 → 内容是否实质完整?
├─ 抽样 2-3 个子文件 → 是否回答了研究问题?
├─ coverage/credibility 不达标是算法/来源问题 ≠ 内容缺失?
│ ├─ 是 → 选项 1(conclude --adopted + 手动修 decay 60d)
│ └─ 否 → 选项 2(补子文件覆盖缺口)
└─ 写报告时明确说明"coverage 是已知算法局限,内容实质完整"
coverage 是辅助指标,不是结论。被数字卡住比被数字忽视更容易出问题。
3. 双层控制回路:机器执行,人做 setpoint
控制论的一个基本判断:复杂系统需要分层,直接控制层负责高频执行,组织层负责低频但关键的价值判断。
graph TD
subgraph 直接控制层_自动
A[每子文件结束<br/>触发 measure] --> B{divergence}
B -->|0.4-0.6| C[切换搜索策略<br/>perplexity → brave]
B -->|> 0.6| D[ESCALATION<br/>通知用户]
B -->|< 0.4| E{coverage}
E -->|< 0.6| F[ACTUATOR 自动回填<br/>未覆盖概念]
E -->|≥ 0.6| G[继续 EXPLORING]
C --> G
F --> G
end
subgraph 组织层_用户
H[Step 0 大纲确认<br/>setpoint 初始化] --> I[Step 1 监控<br/>'完整了吗?']
I --> J[Step 2 终结判断<br/>验收/否决/调整]
D -.升级.-> I
J --> K{ESCALATION 响应}
K -->|继续| G
K -->|调整 setpoint| H
K -->|终止| L[归档]
end
style D fill:#ff6b6b,color:#fff
style H fill:#4a90d9,color:#fff
style J fill:#4a90d9,color:#fff
3.1 直接控制层做什么(机器自动)
直接控制层处理高频、低价值判断、重复执行的任务:
| 操作 | 频率 | 触发条件 | 决策依据 |
|---|---|---|---|
measure | 每子文件结束 | 新子文件 commit | divergence + coverage + credibility |
| 切换搜索策略 | 偶尔 | divergence > 0.4 持续 2 轮 | strategy_failure_count |
| ACTUATOR 自动回填 | 偶尔 | coverage < 0.6 + divergence < 0.3 | uncovered_concepts 列表 |
| ESCALATION 升级 | 罕见 | divergence > 0.6 | severity 判定 |
| Decay 信号检测 | 每天(由 cron 触发) | next_review < now | detect_research_signals() |
这些操作的共同点:有明确规则,机器可以独立完成,不需要价值判断。把它们自动化的目的是让人不用盯着每个子文件审核。
搜索策略决策树(ACTUATOR 实际切换逻辑):
Step 1 进入 → 默认 perplexity(web-search)
↓
每子文件结束 → measure
├─ coverage < 0.6 且有未覆盖概念 → AUTO_FILL(deep_dive 策略补写未覆盖概念)
├─ divergence > 0.6 → ESCALATION → TG 通知用户
├─ divergence > 0.4(且 < 0.6)→ SELF_CORRECTING → 切换 brave / deep_dive
│ 连续 2 次切策略 divergence 不降 → ESCALATION
├─ coverage > 0.8 + divergence ≤ 0.35 + stable ≥ 3 轮 → CONVERGING → 问用户"完整了吗?"
└─ 其他 → EXPLORING(继续当前策略)
切策略之后有一个闭环检查:换成 brave / deep_dive 之后 divergence 如果没有下降(误差 0.02 以内),strategy_failure_count 加 1,连续 2 次无改善才真正触发 ESCALATION——避免一次波动就打扰用户。
实际代码:Actuator.execute() — 执行器动作与 TG 通知模板(research_controller.py)
# 精简自 research_controller.py
class Actuator:
def execute(self, action: dict, context: dict) -> dict:
action_type = action["type"]
slug = context.get("topic_slug", "")
if action_type == "CONTINUE":
return {"ok": True, "action": "continue"}
elif action_type == "SELF_CORRECT":
return {"ok": True, "action": "strategy_switched",
"new_strategy": action.get("strategy")}
elif action_type == "ESCALATE":
tg_send(
f"🚨 *CONTENT ESCALATION*\n\n"
f"调研 *{slug}* 内容严重偏离大纲\n"
f"请回复:\n"
f"· `继续` — 保持当前方向\n"
f"· `调整 setpoint` — 重新定义目标\n"
f"· `终止` — 结束调研"
)
return {"ok": True, "action": "escalated"}
elif action_type == "AUTO_FILL":
concepts = action.get("auto_fill_concepts", [])
tg_send(
f"🔧 *AUTO FILL*\n\n"
f"调研 *{slug}* coverage < 0.6,未覆盖概念:\n"
f"`{' · '.join(concepts)}`\n\n"
f"请补充相关内容后继续。"
)
return {"ok": True, "action": "auto_fill", "concepts": concepts}
elif action_type == "CHECK_COMPLETE":
stable = context.get("coverage_stable_rounds", 0)
tg_send(
f"🤔 *方向稳定,完整了吗?*\n\n"
f"调研 *{slug}* 已连续稳定 {stable} 轮\n"
f"回复 `Y` → `researcher.py conclude {slug} --adopted`\n"
f"回复 `N` → 继续补充"
)
return {"ok": True, "action": "check_complete"}
3.2 组织层做什么(用户在关键决策点介入)
组织层只在三个关键节点介入,其他时间不打扰:
Step 0 — 大纲确认
预调研结束后,AI 给出大纲(经过 perplexity 广度搜索后)。用户审阅:
- “可以” → 执行
researcher.py init + setpoint --init,FSM 进入 EXPLORING - “方向偏了” → 修订大纲,重新确认(参考”两轮修正”模式)
- “补充约束:[X]” → 更新 direction_text,加入新约束
setpoint 是后续所有测量的基准,方向不对 divergence 永远算错。大纲发出后必须明确说”可以”才执行——如果不确定,延迟比跑偏便宜。
Step 1 — 监控
EXPLORING 过程中,每隔几个子文件系统会问一次”完整了吗?“用户看 measure 输出(三指标 + 子文件清单)后回答:
- “Y” → 继续写下一个子文件
- “再加一个:[X 主题]” → 临时加一个指定主题
- “差不多了” → 强制进入 CONVERGING
“内容是否完整”这件事,你自己比算法更清楚——你能感觉到”讲清楚了”,机器没法感觉。
Step 2 — 终结判断
三指标全部达标,FSM 进入 CONVERGING,系统请求最终验收。通读所有子文件和 00-index 的核心结论后决定:
- “通过,conclude” → 执行
researcher.py conclude <slug> --adopted,写入knowledge_conclusions,decay 计时开始 - “X 部分需要补充” → 回到 EXPLORING,补充指定子文件
- “整体方向不对” → 回到 INIT,修订 setpoint
机器能确认”指标达标”,不能确认”内容有用”——这一步的价值判断只能人做。
ESCALATION 响应
divergence > 0.6 触发,Telegram 收到通知后:
- “继续,方向没错” → 回到 EXPLORING,ACTUATOR 切换 deep_dive 策略
- “调整 setpoint” → 回到 INIT,修订方向
- “终止” → 归档当前调研,不写入
knowledge_conclusions
3.3 一个完整的 walkthrough:从”调研 X”到”conclude”
假设用户说”调研一下 GLP-1 类减肥药的作用机制和适用人群”,完整流程是这样。这是人机协同、边跑边学的场景——用户在旁边实时跟进,AI 写每个子文件时用户同步阅读,整个过程 30-45 分钟。
T0:用户提出需求
用户: 调研 GLP-1 类减肥药的作用机制和适用人群
T+5min:预调研阶段
AI 助手:
→ 跑 perplexity 广度搜索 "GLP-1 mechanism weight loss clinical"
→ 写出 11 文件大纲(按五段式结构)
→ 写出 direction_text(从零基础视角,讲清楚 GLP-1 是什么)
→ 通知用户:"这是大纲,5 段式结构 11 文件,方向是 [X]"
T+8min:用户看完大纲,给反馈(Step 0 — 大纲确认)
用户: 补充一个成本章节,其他可以
AI 助手: [更新大纲,加 10-应用-成本.md,重发 direction_text]
用户: 可以
AI 助手: [执行 init + setpoint --init,FSM 进入 EXPLORING]
T+25min:写完前 5 个子文件(用户边读边学,Step 1 — 同步推进中)
[每个子文件结束:先搜索 → 再写 → 跑 measure,用户同时阅读]
AI 助手: [写完 01-原理.md → measure]
→ divergence=0.18 / coverage=0.25 / credibility=2.8
→ 状态:EXPLORING,继续
AI 助手: [写完 02-深入-临床数据.md → measure]
→ divergence=0.21 / coverage=0.45 / credibility=3.2
→ 状态:EXPLORING,继续
... 继续
T+32min:遇到 SELF_CORRECTING
AI 助手: [写完 05-发展-长期.md → measure]
→ divergence=0.42 / coverage=0.65 / credibility=2.9
→ 触发 SELF_CORRECTING:切换搜索策略(perplexity → brave)
→ 状态:SELF_CORRECTING
AI 助手: [写 06-实践-用药.md,使用 brave 搜索]
→ divergence=0.35 / coverage=0.75 / credibility=3.0
→ 状态:回 EXPLORING
T+40min:遇到 ESCALATION(用户当场响应)
AI 助手: [写完 09-应用-决策树.md → measure]
→ divergence=0.67 / coverage=0.85 / credibility=3.1
→ 触发 ESCALATION:通知用户
AI 助手: [TG 推送] "某调研 div=0.67 严重偏离,建议切换策略或重新确认方向"
用户: [当场看到推送] 方向没错,继续写成本章节
AI 助手: [状态:ESCALATION → EXPLORING,切换 deep_dive 策略]
→ 写 10-应用-成本.md
→ measure: divergence=0.18 / coverage=0.95 / credibility=3.2
→ 状态:CONVERGING
T+45min:Step 2 终结判断(用户已经读完大部分内容)
AI 助手: [通知用户]"三指标全部达标,等待验收"
用户: [扫一遍 00-index + 没细读过的子文件]
用户: 通过,conclude
AI 助手: [执行 researcher.py conclude <slug> --adopted]
→ 写入 knowledge_conclusions,field_decay_days=60
→ next_review = now + 60 days
→ FSM 状态:DONE
T+60 天:Decay 触发
[cron 每天跑 detect_research_signals,发现 needs_review]
AI 助手: [TG 推送] "某调研已超过 60 天,需要 verify"
用户: 内容还是对的,still-valid
AI 助手: [执行 verify --still-valid]
→ last_verified = now,next_review = now + 60d
→ confidence: medium → high
整个流程里用户只介入了 4 次——大纲确认、中期监控、ESCALATION 响应、最终验收。其余时间全部自动执行。
“减少用户介入”是个常见误区。用户必须介入的是 setpoint 和终结判断——这两件事是价值判断,机器做不了。让用户逐子文件审核才是反模式。
4. 五段式结构:不预设读者,按”学会”组织
技术调研的”读者”经常是三个月后回头看的自己,那时候什么都忘了;或者是组里另一个对这个领域一无所知的工程师。别假设读者有背景——每个概念首次出现时,一句话定义 + 生活类比 + 机制展开。
五段式是参考模板,不是固定结构(可以灵活调整):
| 段 | 目的 | 回答的核心问题 |
|---|---|---|
| 原理 | 从零基础讲清楚”是什么” | What & Why — 读者能向朋友解释 |
| 深入 | 数据 + 风险 + 边界 | How good/bad — 读者知道可信度和局限 |
| 发展 | 历史 / 未来 / 长期视角 | Evolution — 读者知道为什么是这样 |
| 实践 | 具体怎么做 | How to — 读者能落地执行 |
| 应用 | 个人定制 + 综合判断 + 验收 | Putting together — 读者能做出自己的决策 |
反例:“作为 XYZ 的从业者,你一定知道……”——这种写法默认读者懂 XYZ,调研目的恰恰是让不懂的人学会。
正例:“XYZ 是一个 Y 的 Z(类比:日常生活里的 XXX 就像 Y 一样)。它的核心机制是……”
不是每段都必须有:纯理论调研可能只要”原理 + 深入”,纯实操调研可能只要”实践 + 应用”。强行凑章节会让内容稀薄。
4.1 实例:一个完整的 11 文件调研大纲
为了把五段式讲清楚,这里给一个完整的实例(从减肥药调研的真实目录抽取,11 文件按五段式组织):
| 文件 | 所属段 | 核心内容 | 写作要点 |
|---|---|---|---|
00-index.md | 入口 | 核心结论 + 决策树 + 关键数据 + 否定的方向 | 不是模板填充,是综合判断 |
01-原理.md | 原理 | 是什么 + 减重机制(肠道激素调控) | 一句话定义 + 生活类比:像”肠道给大脑的饱腹信号信使” |
02-深入-临床数据.md | 深入 | STEP 1-4 试验 + 真实世界数据 | 用具体数字(平均减重 15-22%)而非”显著有效” |
03-深入-副作用.md | 深入 | 胃肠道反应 + 罕见风险 | 不回避负面数据,给风险量化(恶心发生率 40-60%) |
04-发展-历史.md | 发展 | 从糖尿病到减肥的”意外发现” | 时间线:2005 上市 → 2010 发现减重 → 2021 FDA 减肥适应症 |
05-发展-长期.md | 发展 | 停药后体重反弹 + 长期安全性 | 长期数据缺口要明确标注(“目前 > 5 年数据有限”) |
06-实践-用药.md | 实践 | 剂量 + 注射频率 + 储存 | 具体可执行:起始 0.25mg 每周,4 周后逐步加量 |
07-实践-饮食.md | 实践 | 与减重饮食的协同 | 不是”健康饮食”,是”蛋白质优先 + 减少精制碳水” |
08-实践-运动.md | 实践 | 抗阻训练防肌肉流失 | 关键洞见:减重 30% 来自肌肉损失,必须抗阻训练 |
09-应用-决策树.md | 应用 | 适用人群 + 禁忌 + 替代方案 | 决策树形式:BMI > 30 → 优先考虑;BMI 27-30 + 合并症 → 评估 |
10-应用-成本.md | 应用 | 国内上市 + 价格 + 医保 | 当前价格区间 + 医保覆盖情况(数据需 verify) |
11-参考资料.md | 引用 | 引用清单(双向绑定格式) | 每条引用必须含论据 + 佐证双向绑定 |
按五段式重组后,这个调研有原理层有应用层,有临床证据有个人执行决策——读完既能”向朋友解释”,也能”做出自己的决定”。
4.2 为什么这种结构能”教”人
五段式是渐进式深化——每一段都比前一段更具体、更落地。
flowchart LR
A["原理<br/>抽象度: 高<br/>概念层"]
B["深入<br/>抽象度: 中<br/>证据层"]
C["发展<br/>抽象度: 中<br/>时间层"]
D["实践<br/>抽象度: 低<br/>操作层"]
E["应用<br/>抽象度: 最低<br/>决策层"]
A --> B --> C --> D --> E
style A fill:#4a90d9,color:#fff
style B fill:#7eb0d9,color:#fff
style C fill:#a8d5e2,color:#333
style D fill:#f7c59f,color:#333
style E fill:#ff6b35,color:#fff
| 段 | 抽象度 | 读者收获 |
|---|---|---|
| 原理 | 高(概念层) | “是什么,为什么能减重” |
| 深入 | 中(证据层) | “效果多大、风险多大、边界在哪” |
| 发展 | 中(时间层) | “这药怎么来的、长期会怎样” |
| 实践 | 低(操作层) | “我要用,具体怎么用” |
| 应用 | 最低(决策层) | “我要不要用,值不值” |
原理段读完知道”是什么”,深入段读完知道”该不该信”,发展段知道”会不会过时”,实践段知道”怎么用”,应用段知道”我要不要”。
反模式是按”先查到的先写”组织——技术细节扎堆,读完不知道”这东西对我有什么用”。五段式强制按认知顺序排:先心智模型,再证据,再操作,最后决策。
4.3 写作前自检:每个文件首段必须有”什么是 X”
每个子文件的第一段,必须用以下模板开头:
# [文件名]
> **一句话定义**:X 是一个 Y 的 Z(生活类比:[类比])。
[下面才是机制展开、数据、风险等]
每章先告诉读者这是什么,不要直接跳进技术细节。
4.4 代码验证类调研:写代码 + 跑实测 + 纳入引用
五段式有一个特殊变体——代码验证类调研。某些主题的结论不能靠读资料得出,必须写代码跑实测,把实测数据作为论证核心。这类调研在 Step 1 会多出一个子步骤(Step 1.5),产物落入调研目录的 4 个特殊子目录。
4.4.1 什么时候需要写代码验证
不是所有调研都要写代码。判定标准:
| 调研类型 | 需要代码验证? | 原因 |
|---|---|---|
| 概念性主题(哲学/历史/方法论) | 否 | 结论来自文献综合,不需要量化 |
| 工具使用/操作流程 | 否 | 命令本身可复制运行,不需要实测 |
| 算法/库的能力对比 | 是 | 性能、准确度、量化指标必须实测 |
| 工具/产品的能力边界 | 是 | 文档说能 ≠ 真能做,必须跑 |
| FSM 本身的科学验证 | 是 | FSM 是这套 SOP 的核心,需要用对照实验证明它有效 |
具体例子:
- 音频响度归一化(LUFS)调研:要回答”我的音频响度对不对”,必须跑实测——把同一段音频用 ffmpeg / pyloudnorm / 自实现 K-weighting 三种方法算,看三方数字差异是否 ≤ 0.04 LU。文档不能直接告诉你”差多少”。
- YOLO 和 MediaPipe 在姿态识别的对比:文档说”准确率 90%+“是宣传,必须跑实测——在不同光照、遮挡、距离下各跑 100 张图,看实际能正确检测多少。
- FSM 调研系统的科学验证:FSM 是这套 SOP 的核心,必须用 Claude Code CLI 做对照实验,证明 FSM 真的能识别偏离、自动校正。
4.4.2 流程差异:Step 1 多出 1.5 子步骤
正常 Step 1(搜索 → 写 → commit),代码验证类调研多出三个动作:
flowchart LR
subgraph "Step 1 标准"
A1[搜索] --> A2[写子文件] --> A3[跑 measure] --> A4[commit]
end
subgraph "Step 1.5 代码验证(多 3 个动作)"
B1[搜索] --> B2[写子文件] --> B3[写验证脚本] --> B4[跑实测] --> B5[把数据挂引用] --> B6[commit]
end
实测数据不是”参考资料”,是调研自己的产出——论据字段填”实测数据:my = -22.29 LUFS / ffmpeg = -22.3 / pyloudnorm = -22.332”,不是”参考某个网页”。
4.4.3 子目录约定(4 个特殊目录)
代码验证类调研的产物落入 4 个特殊子目录,不属于正式内容(不进引用质量检查扫描):
| 子目录 | 放什么 | 例子 |
|---|---|---|
source/ | 搜索响应原始输出 | source/pplx_q1.json(perplexity API 的原始响应,含完整 content + citations) |
scripts/ | 可执行代码 | scripts/lufs_meter.py(三方对比脚本)、scripts/validate_yolo.py(YOLO 实测脚本) |
assets/ | 图片、附件 | assets/diagram.png、assets/test_audio.wav |
templates/ | 模板文件 | templates/citation-entry.md(引用条目模板) |
check_citation_quality.py 默认排除这 4 个目录(--include 参数强制扫描)——它们是事实库和工具箱,不是调研正文。
4.4.4 真实可运行的命令示例(LUFS 三方实测)
从实际 LUFS 调研中抽一段。命令全部真实可运行,接口经过实测验证:
# 1. 装依赖
pip install pyloudnorm ffmpeg-python numpy
# 2. 准备测试音频(任意 wav 文件)
# 测试文件:assets/test_audio.wav(48kHz,立体声,1 分钟)
# 3. 跑三方对比(脚本在 scripts/lufs_meter.py)
python scripts/lufs_meter.py assets/test_audio.wav
输出示例(实测数字):
my K-weighting 实现: -22.29 LUFS
ffmpeg loudnorm: -22.30 LUFS
pyloudnorm (ITU-R): -22.332 LUFS
三方差: ≤ 0.04 LU
怎么把这些实测数据写进调研:
**§2.2 三方实测验证**:三种 LUFS 实现对同一音频的测量结果一致(差 ≤ 0.04 LU),
证明 K-weighting 滤波 + Gating 算法是可复现的[^实测]。
[^实测]: [内部实测] LUFS 三方对比 — scripts/lufs_meter.py
| 论据:①my K-weighting -22.29 LUFS ②ffmpeg loudnorm -22.30 LUFS
③pyloudnorm -22.332 LUFS ④差 ≤ 0.04 LU
| 佐证:§2.2
注意来源类型用了 [内部实测]——这是引用类型白名单里没的标签?其实白名单只有 6 个(官方/论文/博客/社区/新闻/官方文档),[内部实测] 需要在脚本里扩展。但更重要的是:实测数据本身就是论据,不需要 URL——只需要”这个实测在哪份脚本里”作为引用锚点。
4.4.5 为什么不能跳过实测
“读文档抄数据”和”跑实测得数据”的区别,在 SOP 里意味着三件事:
- 可复现性:实测脚本留在
scripts/目录,任何人可以复现你的结论。文档抄的数据无法验证。 - 争议点暴露:实测会发现文档没说的问题——比如 ffmpeg 默认输出 integrated LUFS,但短视频需要 short-term / momentary,文档不会主动告诉你”差在哪”。
- 应对未来变化:实测脚本是”持续验证”的基础——工具版本升级时,跑一遍脚本就知道结论是不是要更新。文档则需要重新读一遍才能判断。
代码验证类调研的成本高(写脚本、跑实测、调错),但论证质量也高一档——“实测差 ≤ 0.04 LU”比”三种工具都说能算 LUFS”可信得多。
5. 引用规范:论据 + 佐证双向绑定
调研不是 URL 堆叠。每条引用必须显式声明:这个来源提供了什么(论据),在本文哪里被使用(佐证)。写引用时先想清楚”我引用它是因为什么”——杜绝挂着权威来源、实际论证却不依赖它的情况。
6 字段格式,每条引用一行 + 管道分隔:
[^1]: [来源类型] 标题 — URL | 论据:①<具体事实/数据> ②<...> | 佐证:§X, §Y
| 字段 | 含义 | 示例 |
|---|---|---|
[^N] | 引用编号(正文挂载锚点) | [^1] |
[来源类型] | 白名单:官方/论文/博客/社区/新闻/官方文档 | [论文] |
| 标题 | 来源的可读名 | FasterWhisper: 4x faster Whisper inference |
| URL | 真实可访问地址 | https://github.com/SYSTRAN/faster-whisper |
| 论据 | 这个来源提供了什么 | ①CTranslate2 优化实现 ②INT8/FP16 量化 ③比原版快 4 倍基准 |
| 佐证 | 在本文哪些段落使用 | §2.1, §3 速度对比 |
反例(裸 URL):
“FasterWhisper 比原版快 4 倍。https://github.com/SYSTRAN/faster-whisper”
读者看到这句话,无法判断这个 URL 是支持”快 4 倍”这个论断,还是支持其他什么——它就是悬在那儿。
正例(双向绑定):
“FasterWhisper 比原版快 4 倍[^1]。”
文末有:
[^1]: [官方] SYSTRAN/faster-whisper — https://github.com/SYSTRAN/faster-whisper | 论据:①CTranslate2 优化实现 ②INT8/FP16 量化 ③比原版快 4 倍基准 | 佐证:§2.1, §3
读者扫一眼文末就能看出”这个来源支持了本文哪几段”,快速判断值不值得点开。
机械检查:commit 前跑 check_citation_quality.py <file>,exit 0 才能 commit(检查来源类型白名单、论据/佐证覆盖率、裸链率)。
6. Decay 机制:知识会过时,自动复查
调研完成不等于永远正确。算法版本更新、行业格局变化、新研究出现——结论是有保鲜期的,这套机制就是管保鲜期的。
60 天硬上限(对工程/工具类):任何调研无论主题,decay 周期都不超过 60 天。经典理论(数学定律、物理常数)可放宽到 180 天,但要标注”经典理论例外”。
decay_days 矩阵(背景 × 目标):
| 原理 | 实操 | 概览 | |
|---|---|---|---|
| 零基础 | 180d | 90d | 45d |
| 有了解 | 90d | 45d | 30d |
| 想深入 | 45d | 30d | 15d |
注:任何背景 × 目标组合算出来的天数不得 > 60(60 天硬上限)。经典理论(数学定律、物理常数)例外,可保留 180d,但必须在 00-index.md 注明原因。实际实现是 infer_decay(background, goal) 函数,在 researcher.py init 时自动写入 frontmatter。
矩阵的推导逻辑(主观判断,可按领域校准):
- 行(背景深度):越深入,学习曲线越陡,发现新变化越快——“想深入”的行比”零基础”的行 decay 更短
- 列(目标类型):概览(格局、玩家、市场现状)变化最快;实操(工具、API)居中;原理(机制、数学基础)最慢变
60 天硬上限来自一个个人判断:工程领域主要框架的 major release 周期通常在 3-6 个月,60 天留出缓冲又不至于太宽松。AI/ML 领域可能 30 天更合适;数学/物理原理类可以放宽到 180 天。矩阵里的天数是起点,跑几个月之后可以根据自己领域的实际变化速度重新标定。
decay 周期就是保质期。咖啡豆烘焙后 7 天最好,面包上架 3 天变硬,牛奶 14 天后下架——知识也一样,明确标注”这个 60 天后可能不准”比假装永远新鲜更诚实。
flowchart TD
A[调研完成<br/>conclude --adopted] --> B{60 天到了}
B -->|否| C[保持 ADOPTED<br/>知识库继续使用]
B -->|是| D[detect_research_signals<br/>触发复查]
D --> E{读 00-index +<br/>抽样 2-3 子文件}
E -->|方向对<br/>数据未过期| F[verify --still-valid<br/>decay 重置 60 天]
E -->|方向对<br/>但数据过期| G[verify --update<br/>走补丁流程]
E -->|核心结论被推翻| H[verify --outdated<br/>归档旧调研]
F --> C
G --> I[增量搜索 +<br/>内容更新 +<br/>重新 measure]
I --> F
H --> J[启动新调研]
style F fill:#4caf50,color:#fff
style G fill:#ff9800,color:#fff
style H fill:#f44336,color:#fff
三种 verify 路径:
| 路径 | 何时用 | 动作 |
|---|---|---|
--still-valid | 方向对、数据未过期 | decay 重置 60 天,confidence 从 medium 升到 high |
--update | 方向对、但数据过期 | 增量搜索 + 补丁现有子文件 + 重新 measure(不重置 FSM state) |
--outdated | 核心结论被推翻 | 归档旧调研,启动新调研 |
实际代码:cmd_conclude() — conclude 写入 knowledge_conclusions + 启动 decay 计时(researcher.py)
# 精简自 researcher.py
def cmd_conclude(topic_slug, conclusion_type, project_ref=None, note=None):
conc = db_get_conclusion(topic_slug)
# 原则 4:conclude 前自动扩充 CREDIBILITY_SCORES 字典
# 扫描该调研所有 .md(排除 source/assets/templates/scripts)
modules_for_expand = [f.read_text() for f in topic_dir.rglob("*.md")
if not any(p in {"source","assets","templates","scripts"}
for p in f.relative_to(topic_dir).parts)]
auto_expand_report = rc.auto_expand_credibility_dict(modules_for_expand)
ts = now_ts()
if conclusion_type == "adopted":
db_update_conclusion(
topic_slug,
status="completed",
conclusion_type="adopted",
conclusion_note=note,
project_ref=project_ref,
last_verified=ts,
next_review=ts + conc["field_decay_days"] * 86400, # decay 计时从这里开始
needs_review=0,
)
tg_send(f"✅ 调研 *{conc['topic_title']}* 结论已被采纳(项目:{project_ref or '—'})")
return {
"ok": True,
"next_review_in_days": conc["field_decay_days"],
"auto_expand": auto_expand_report,
}
elif conclusion_type == "refuted":
db_update_conclusion(topic_slug, status="contradicted",
conclusion_type="refuted", needs_review=1)
tg_send(f"⚠️ 调研 *{conc['topic_title']}* 结论被推翻")
elif conclusion_type == "superseded":
db_update_conclusion(topic_slug, status="superseded",
conclusion_type="superseded", needs_review=0)
conclude --adopted 做了两件事值得注意:
next_review = ts + field_decay_days * 86400——decay 计时从这一刻开始,不是从调研完成时开始auto_expand_credibility_dict()——自动把调研中引用的权威/专业级域名加进可信度字典,无需每次手动维护
6.1 decay_digest:把”哪些调研要复查”主动推送给用户
调研完成后不是”等着被想起”,而是系统主动追踪。具体机制是 detect_research_signals() 函数(详见 2.3 节的三层信号检测)——这里讲它怎么跟 cron 和每日反思配合。
关键设计:不要让用户”记得回来复查”。60 天后你大概率忘了这个调研的存在,或者记得但没动力去做。Decay 机制把”记得复查”这件事变成系统主动推送——AI 助手在调研快过期时主动通知”某调研即将过期,是否复查?”,用户只需要回答 Y/N。
6.2 cron 调度:detect_research_signals 何时跑
Decay 检查不是 ad-hoc 触发,而是嵌在每日反思里——每天 23:45 跑一次,作为日常基础设施的一部分:
# daily-reflection 任务里的关键一段(伪代码)
def daily_research_check():
"""每天 23:45 跑,作为 daily-reflection 的一部分"""
signals = detect_research_signals() # 三层信号检测
for sig in signals:
if not tracker_check_exists(sig["type"], sig["value"], within_hours=6):
tg_send(f"[decay] {sig['detail']}") # TG 推送
tracker_insert(sig, verify_by=now() + 72*3600) # medium 72h / high 48h
detect_research_signals() 三层检测的具体逻辑(简版):
| 信号类型 | 检测对象 | 触发条件 | 严重度 |
|---|---|---|---|
RESEARCH_STALL_ACTIVITY | 进行中调研的停滞 | in-progress 状态 > 14 天无更新 | high |
RESEARCH_STALL_ACTIVITY | 初期调研停滞 | in-progress 状态 > 3 天无更新 | medium |
RESEARCH_STALL_TEMPORAL | 已完成调研的过期 | next_review < now() | high(若 contradicted)/ medium |
CONTENT_DIVERGENCE | 进行中调研的严重偏离 | FSM 处于 ESCALATION 状态 | high |
为什么不单独跑 cron?因为 decay 检查需要”用户行动”(verify 三选一),把它嵌在 daily-reflection 里,用户每天 23:45 收到一条汇总消息,里面既有”今天做了什么”,也有”哪些调研要复查”,信息密度高且不打断。
6.3 完整的 decay 工作流:从 cron 触发到用户响应
flowchart LR
T0["T0<br/>23:45 cron 触发"] --> T1["T+30s<br/>信号去重 + TG 推送"]
T1 --> T2["T+1 天<br/>用户读 00-index + 抽样判断"]
T2 --> T3{"用户响应?"}
T3 -->|Y| T4["T+1 天完成<br/>执行 verify 命令"]
T3 -->|N| T5["T+2-3 天<br/>超期: stale + 再推"]
T5 --> T3
style T0 fill:#4a90d9,color:#fff
style T4 fill:#4caf50,color:#fff
style T5 fill:#ff9800,color:#fff
T0:cron 触发(每天 23:45)
daily-reflection cron 触发 LLM,运行 detect_research_signals(),得到一组信号列表(可能 0-N 个)。
T+30s:信号去重
每个信号先去 tracker 表查 6 小时内是否已有相同记录。如果有,跳过(避免反复骚扰用户)。如果没有,继续。
T+30s:TG 推送
对每个新信号,直接发到用户的 Telegram,格式:
[decay] 调研 *某主题* 已超过 60 天 decay 周期,需要 verify
同时设 verify 期限:medium severity → 72h,high severity → 48h。
T+1 天:用户打开 TG 看到通知
用户的实际工作流:
- 打开通知 → 看到是哪个调研过期
- 快速读 00-index → 回忆这个调研是什么
- 抽样 2-3 个子文件 → 判断”内容还对不对”
- 决定 verify 路径:
- 内容完全对 →
--still-valid - 部分数据过期 →
--update(走补丁流程) - 核心结论错了 →
--outdated
- 内容完全对 →
- AI 助手执行命令:
researcher.py verify <slug> --still-valid - AI 助手回报结果:
[verify] 某主题 verify --still-valid 成功 next_review: 2026-08-16(60 天后) confidence: medium → high
T+2-3 天:超期未响应
如果用户没在 48h/72h 内响应,系统标记为 stale + 再发一次 TG:
[decay] 调研某主题仍未处理(已超过 48h verify 期限)
这给用户第二次提醒机会,避免”忘记 → 永远过期”。
6.4 daily-reflection 与 decay 信号的聚合
daily-reflection cron(每天 23:45)不只是检查 decay,它还聚合当天所有重要事件:
- 完成的调研(
conclude事件) - decay 即将到期的调研
- 当天的 daily_log 提炼
- 重要的 lessons / decisions
daily-reflection 里 decay 信号优先展示——这些是需要用户行动的事件,比纯日志更重要。
聚合逻辑(伪代码):
def daily_reflection():
# 1. 扫描当天 session,写入 daily 文件
scan_sessions_to_daily()
# 2. 检查调研状态(关键!decay 检查在这里)
research_signals = detect_research_signals()
for sig in research_signals:
push_to_telegram(f"[decay] {sig['detail']}")
# 3. 提炼 lessons / decisions
distill_lessons()
# 4. 推送最终摘要到 TG
push_daily_summary()
decay 检查不单独跑 cron,嵌在每日反思里——每天 23:45 一条 TG 消息,信息密度高,不打断。
6.5 补丁流程(--update 详细)
适用场景:
verify --update后需要补充内容;或内容方向对、但有部分数据/案例过时。
与 verify 流程的关系:
verify --update(标记为 active + medium confidence)
↓
进入"补丁"子流程(本节)
↓
完成补丁后 → verify --still-valid → 重置 decay 计时器 → confidence: medium → high
Step 3.1:判定走哪个 verify 分支
flowchart TD
A[收到 RESEARCH_STALL_TEMPORAL 信号] --> B[读 00-index.md\n抽样 2-3 个子文件]
B --> C{判断}
C -->|内容方向对、数据未过期| D[verify --still-valid]
C -->|内容方向对、但有部分数据过时| E[verify --update\n走补丁流程]
C -->|核心结论被推翻 / 领域已转向| F[verify --outdated]
style D fill:#4caf50,color:#fff
style E fill:#ff9800,color:#fff
style F fill:#f44336,color:#fff
Step 3.2:补丁执行 4 步
- 3.2.1 用 perplexity 增量搜索
[主题] + [时间窗口] latest news,锁定 1-3 个有变化的具体点(主线程执行,sub-agent 在 web 搜索上不可靠) - 3.2.2 决策补丁方式(改
00-index关键数据 / 新增子文件 / 追加章节) - 3.2.3 patch 现有文件 + 跑
measure检查 divergence 是否上升 + 逐文件 commit(commit message 格式:patch(T<num>): <描述>) - 3.2.4
verify --still-valid重置 decay 计时器为 60 天,confidence: medium → high
measure 的副作用(实测):
- 写
SETPOINT.json(coverage_history / stable_rounds / last_measure_at) - 写
state.db的research_fsm.setpoint_json字段(双写缓存) - 不改
research_fsm.state列(FSM state 保持 CONVERGING/DONE 不变) - 不写
knowledge_conclusions(那是verify --still-valid才写)
Step 3.3:T46 灰色状态特殊路径
有一种情况不走补丁:内容完整,但 FSM 指标不达标(state=INIT,coverage 算法偏低)。这种情况不应该走补丁路径(因为没有 conclude),唯一的路径是:
conclude --adopted(手动执行,绕过指标检查)
↓
60 天后正常走 verify 流程
指标不达标不等于内容有问题——Bailian embedding 对中文 direction_text 的概念抽取碎片化,可能导致 coverage 显示 0.3-0.5,但实际内容已经完整回答了研究方向。这种情况用人工判断覆盖算法判断,在 conclude 报告里注明”coverage 是已知算法局限,内容实质完整”。
6.6 为什么不让用户”记得回来复查”?
人不会记得。60 天后大概率已经忘了这个调研的存在,或者记得但没动力去做。Decay 机制把”记得复查”变成系统主动推送——到期了 TG 通知你,你只需要回答 Y/N。
不依赖记忆力和意志力,把它们编码进系统——这是这整套设计反复出现的一个判断。
7. 设计考量:为什么这样做,而不是那样做
做完系统之后,留几个影响整体架构的判断——不是”这样更好”,而是”换成另一种做法会出什么问题”。
7.1 原则放 SOP,工具放 Skill
SOP 是 canonical 定义(放哪里都不变),Skill 是 self-contained 入口(读者从 Skill 也能看懂,无需跨文件跳转)。有分歧以 SOP 为准。
原则需要稳定,工具需要灵活:
- 原则(调研 ≠ 博客、五段式、引用双向绑定、decay 60 天硬上限)是个人决策的产物,改了就有”业务规则变了”的含义——必须集中存一处,避免分散定义导致不一致。
- 工具(怎么调 perplexity API、怎么跑
check_citation_quality.py)是工程实现细节,可以随版本升级而变化——按需调整,不需要每次涉及决策。
判断方法:如果一句话能放在 SOP 顶部,不要写在 Skill 里。Skill 只承载”如何用工具执行这些原则”。
Skill 自我进化的风险:Hermes 的 Skill 有自学习机制——AI 在执行过程中发现更好的做法,会直接 patch Skill 文件。这是好事,但有一个副作用:Skill 里的描述会悄悄漂移,逐渐偏离 SOP 的原则,而你不一定察觉。
因此 Skill 层的更新只作为 patch——每次执行会有 Skill 的补丁和经验,积累足够的使用证据。在每日/每周反思中,再判断这些 patch 是否应该合并回 SOP:
Skill 自动 patch(AI 执行中发现更好做法)
↓
在 daily / weekly reflection 中检视 Skill 变更
↓
判断:这是工具细节的优化,还是原则层面的改变?
├─ 工具细节 → 留在 Skill,不动 SOP
└─ 原则改变 → 手动合并进 SOP,同步更新 Skill 以 SOP 为准
不要让 Skill 的自动漂移替代 SOP 的有意识决策——Skill 是试验场,SOP 是裁决。
这条原则在实际架构上的落地:原来 perplexity / brave-search / duckduckgo-search 是三个独立 Skill,deep-dive / cross-validate 也是两个 Skill。重组之后:三个搜索 Skill 合并为单一 web-search umbrella(含自动 fallback),deep-dive 和 cross-validate 的方法论迁入 execute.md(workflow),只保留工具 API 层在 Skill 里。深度挖掘和交叉验证的做法是 SOP 原则,不能因为换了搜索工具就变——这正是在 weekly reflection 里做出的判断,而不是让 Skill patch 自动决定的。
7.2 几个反直觉的设计选择
不要写”流程文档”,要写”反馈系统”。传统 SOP 是”第一步做什么、第二步做什么”的线性步骤——但调研是有回路的,你会在第 5 步回头改第 1 步的方向。线性文档无法表达这个。状态机 + 反馈回路可以。
不要追求”全自动”。调研的核心是 setpoint 和终结两个价值判断,这两件事没法机器做。硬要全自动的结果是”机器的偏见被自动化”。
不要堆砌工具。tools 多 ≠ SOP 好。调研 SOP 的核心是反馈回路(三指标 + 收敛四元 + decay 三分支),工具是执行器,不是回路本身。
不要写展望章节。如果有内容就写,没有就不写。“AI 调研 + 控制回路 + SOP 系统”这条线已经形成闭环,不需要强行塞”未来展望”。这种章节在工程文档里是噪音——读者不需要你预测未来,他们需要你现在能用的工具。
8. 这套思路能搬到哪里
8.1 Wikipedia 漫游 → 控制论 → 调研系统
回头看开头那个类比。Wikipedia 漫游里的校正机制——“这个概念没搞清楚,我要回去补”——用控制论的语言描述,就是:
| 控制论角色 | 漫游时的直觉 | 调研系统对应 |
|---|---|---|
| SENSOR(传感器) | “感觉跑偏了 / 没覆盖全 / 来源不对” | sensor_measure():计算 divergence + coverage + credibility |
| COMPARATOR(控制器) | “这个判断有没有超出我的容忍范围” | FSM 状态转移判定(三指标阈值) |
| ACTUATOR(执行器) | “要回去补 / 换个来源 / 放弃这条线” | 切换搜索策略 + 自动回填 + ESCALATION 升级 |
| SETPOINT(目标值) | “我原本想搞清楚的那件事” | 用户确认的方向文本(direction_text) |
“调研写得好”和”调研 SOP 写得好”是两件不同的事。前者是执行层的问题(写子文件质量),后者是跑偏时能不能自我纠正。
8.2 复用性:反馈回路可以搬到任何”质量靠人盯”的场景
反馈回路不只适用于调研。任何”质量靠人盯”的场景,都可以用同样思路改造:
- 代码评审:SENSOR = linter / test coverage / complexity 测量;COMPARATOR = 是否达到 PR 标准;ACTUATOR = 阻断 merge / 要求补充测试。
- A 股复盘:SENSOR = 当日盈亏 + 偏离预期;COMPARATOR = 是否触发止损止盈;ACTUATOR = 强制平仓 / 调整仓位。
- 内容创作:SENSOR = 阅读量 / 跳出率 / 评论质量;COMPARATOR = 是否达到发布标准;ACTUATOR = 触发重写 / 撤稿。
- 项目管理:SENSOR = 进度偏差 + 风险信号;COMPARATOR = 是否触发升级;ACTUATOR = 调整资源 / 启动应急。
共同模式:让”质量评判”从主观感受变成可测量的数字,让”异常处理”从临时反应变成预设路径。
8.3 分工:机器跑回路,人定目标
第 3 节把调研拆成两层——直接控制层和组织层。两层各做各的事,边界清楚,系统才稳。
直接控制层负责高频、有规则的事:每子文件结束跑 measure,coverage 不够就自动回填,divergence 超标就切策略或 ESCALATION。这些判断有阈值、有代码,机器可以独立完成。
组织层只在少数节点介入:Step 0 确认 setpoint,Step 1/2 判断”完整了吗”,ESCALATION 时决定方向对不对。这三处都是价值判断——内容有没有用、方向要不要改——机器做不了。
两个常见误区:
- 让机器代替人做终结判断 → 机器的偏见被自动化,调研变成”指标达标但没人要”的废纸。
- 让人逐步审核每个子文件 → 反馈回路白建了,人又退回流水线质检。
正确分工:执行层按 setpoint 跑到底,组织层只在关键决策点介入。
调研的工程化不是写更多文档,是让每一步都可以被测量、被校正、被推送。