我的实习生 - 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 的可信度加权平均分。

计算过程

  1. 扫描所有 URL,根据域名在 CREDIBILITY_SCORES 字典里查分
  2. 未登记域名给 2.0 兜底分(不奖不惩,避免”新出现但真实的来源”被零分惩罚)
  3. 裸链率(不在 [^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.orggithub.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每子文件结束新子文件 commitdivergence + coverage + credibility
切换搜索策略偶尔divergence > 0.4 持续 2 轮strategy_failure_count
ACTUATOR 自动回填偶尔coverage < 0.6 + divergence < 0.3uncovered_concepts 列表
ESCALATION 升级罕见divergence > 0.6severity 判定
Decay 信号检测每天(由 cron 触发)next_review < nowdetect_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.pngassets/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 里意味着三件事:

  1. 可复现性:实测脚本留在 scripts/ 目录,任何人可以复现你的结论。文档抄的数据无法验证。
  2. 争议点暴露:实测会发现文档没说的问题——比如 ffmpeg 默认输出 integrated LUFS,但短视频需要 short-term / momentary,文档不会主动告诉你”差在哪”。
  3. 应对未来变化:实测脚本是”持续验证”的基础——工具版本升级时,跑一遍脚本就知道结论是不是要更新。文档则需要重新读一遍才能判断。

代码验证类调研的成本高(写脚本、跑实测、调错),但论证质量也高一档——“实测差 ≤ 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 矩阵(背景 × 目标):

原理实操概览
零基础180d90d45d
有了解90d45d30d
想深入45d30d15d

注:任何背景 × 目标组合算出来的天数不得 > 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 做了两件事值得注意:

  1. next_review = ts + field_decay_days * 86400——decay 计时从这一刻开始,不是从调研完成时开始
  2. 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 看到通知

用户的实际工作流:

  1. 打开通知 → 看到是哪个调研过期
  2. 快速读 00-index → 回忆这个调研是什么
  3. 抽样 2-3 个子文件 → 判断”内容还对不对”
  4. 决定 verify 路径:
    • 内容完全对 → --still-valid
    • 部分数据过期 → --update(走补丁流程)
    • 核心结论错了 → --outdated
  5. AI 助手执行命令:
    researcher.py verify <slug> --still-valid
  6. 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.dbresearch_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-divecross-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 跑到底,组织层只在关键决策点介入。

调研的工程化不是写更多文档,是让每一步都可以被测量、被校正、被推送。

评论