1
00:00:00,100 --> 00:00:06,432
本内容改编自小山学堂《学 AI 产品，从入门到精通》，为二次演绎配音版。

2
00:00:06,382 --> 00:00:07,548
第四集。

3
00:00:07,548 --> 00:00:19,795
前三集我们看了骨架、默认值和工具层，这一集看三块更偏系统的东西，记忆检索、一个叫 Dream 的记忆整理机制，还有子 Agent 的配置是怎么合并出来的。

4
00:00:19,795 --> 00:00:22,956
先说为什么这三块值得单独讲。

5
00:00:22,956 --> 00:00:28,798
它们有一个共同特征，都是看起来很简单、实际有一堆降级路径的设计。

6
00:00:28,798 --> 00:00:38,377
记忆检索，你以为就是搜一下，实际上有同步、全文检索、向量检索、加权、重排六步，而且每一步都可能降级。

7
00:00:38,377 --> 00:00:46,622
Dream，你以为就是定期整理记忆，实际上有三道门控、一把不保证互斥的锁，还有一套失败回滚。

8
00:00:46,622 --> 00:00:57,199
子 Agent 配置合并，你以为就是覆盖，实际上是逐字段级联，而且有些字段只存在于某些结构里，搞错了就会得出错误的优先级。

9
00:00:57,199 --> 00:01:04,423
这三块的共同主题是，生产系统真正复杂的地方不在主流程，在于出问题时怎么办。

10
00:01:04,423 --> 00:01:06,033
我们开始。

11
00:01:06,196 --> 00:01:08,225
先看记忆检索。

12
00:01:08,175 --> 00:01:22,225
查询之前先同步脏文件，然后结合 FTS5 的 BM25 和可选的 sqlite-vec 做 KNN，合并分数经过时间衰减、来源权重和访问增益，最后可选启用 MMR 多样性重排。

13
00:01:22,225 --> 00:01:25,062
六步，我们按顺序过一遍。

14
00:01:25,062 --> 00:01:27,358
第一步，查询前同步。

15
00:01:27,358 --> 00:01:37,394
MemoryFileWatcher 累积变化的 Markdown 路径，backend 在 search 开始的时候重新索引新增或修改的文件，并删除已移除文件的旧 chunk。

16
00:01:37,394 --> 00:01:44,545
这一步的意义是，你在编辑器里手改了记忆文件，下一次查询就能看到，不用重启。

17
00:01:44,545 --> 00:01:48,103
第二步，全文检索出 BM25 候选。

18
00:01:48,103 --> 00:01:53,115
先执行普通 FTS，再补充 global 和 workspace 来源的查询。

19
00:01:53,115 --> 00:02:00,398
为什么要补充，因为 session 数量多的时候，会把自己真正需要的长期记忆挤出候选集。

20
00:02:00,398 --> 00:02:02,958
第三步，可选向量检索。

21
00:02:02,958 --> 00:02:07,466
只有在 sqlite-vec 和 provider 都可用的时候才嵌入 query。

22
00:02:07,466 --> 00:02:09,966
第四步，归一化与合并。

23
00:02:09,966 --> 00:02:13,980
BM25 分数和向量 L2 距离分别归一化。

24
00:02:13,980 --> 00:02:20,819
双路命中的时候按权重合并，同时保证合并结果不低于这个 chunk 的 FTS 分数。

25
00:02:20,819 --> 00:02:26,973
最后这一句很关键，它保证加了向量检索不会让原本的全文结果变差。

26
00:02:26,973 --> 00:02:29,593
第五步，时间与来源加权。

27
00:02:29,593 --> 00:02:36,324
session 按半衰期做指数衰减，global 和 workspace 被视为 evergreen，也就是不衰减。

28
00:02:36,324 --> 00:02:40,050
然后再乘来源权重和适度的访问增益。

29
00:02:40,050 --> 00:02:42,946
第六步，可选多样性重排。

30
00:02:42,946 --> 00:02:50,374
开启后按相关性和 snippet 的 Jaccard 差异做贪心重排，最后截断到 max_results。

31
00:02:50,374 --> 00:02:54,990
还有个细节，候选数量是 max_results 乘三。

32
00:02:54,990 --> 00:02:59,809
先宽召回再精排，这是检索系统的常见做法。

33
00:02:59,956 --> 00:03:05,146
这条流水线里有两个开关最容易被误读，单独拎出来讲。

34
00:03:05,096 --> 00:03:07,944
第一个，embedding 失败会怎样。

35
00:03:07,944 --> 00:03:14,687
答案是向量路径停止，FTS 结果仍然进入 hybrid_search_merge。

36
00:03:14,687 --> 00:03:19,747
源码里会记录一条 warning，然后传 None 继续走 FTS-only。

37
00:03:19,747 --> 00:03:24,771
所以正确的说法是 fallback 等于 FTS-only，不是整次搜索失败。

38
00:03:24,771 --> 00:03:29,267
页面和调用方都不该把 embedding 故障当成搜索失败。

39
00:03:29,267 --> 00:03:34,819
这是很标准的渐进增强设计，向量检索是加分项，不是必需品。

40
00:03:34,819 --> 00:03:37,908
第二个，MMR 默认是什么状态。

41
00:03:37,908 --> 00:03:42,512
MmrConfig 的 Default 实现里，enabled 是 false，lambda 是零点七。

42
00:03:42,512 --> 00:03:44,531
重点是这个零点七。

43
00:03:44,531 --> 00:03:50,384
它不会因为你没开 MMR 就生效，只有显式开启 MMR 之后才参与计算。

44
00:03:50,384 --> 00:03:56,394
很多人看到配置里有 lambda 零点七，就以为重排默认在跑，这是错的。

45
00:03:56,394 --> 00:03:57,944
默认不重排。

46
00:03:57,944 --> 00:04:03,846
记牢这两条，embedding 失败退化为纯全文，MMR 默认关闭。

47
00:04:03,988 --> 00:04:06,846
第二个话题，Dream 的真实机制。

48
00:04:06,796 --> 00:04:12,469
Dream 会把近期 session 日志和现有的 MEMORY.md 合并成长期记忆。

49
00:04:12,469 --> 00:04:20,342
它由会话结束、可选周期检查或者手动命令进入，并且受门控和最佳努力锁共同约束。

50
00:04:20,342 --> 00:04:22,085
先看三道门。

51
00:04:22,085 --> 00:04:23,900
第一道是配置门。

52
00:04:23,900 --> 00:04:26,688
MemoryDreamConfig 的 enabled 默认为 true。

53
00:04:26,688 --> 00:04:29,368
子 Agent 会话直接跳过 Dream。

54
00:04:29,368 --> 00:04:31,291
第二道是时间门。

55
00:04:31,291 --> 00:04:34,272
min_hours 默认为四。

56
00:04:34,272 --> 00:04:40,871
锁文件的 mtime 记录了上次成功 consolidation 的时间，用它判断距离上次过了多久。

57
00:04:40,871 --> 00:04:42,866
第三道是会话门。

58
00:04:42,866 --> 00:04:45,594
min_sessions 默认为三。

59
00:04:45,594 --> 00:04:51,712
统计上次 consolidation 之后修改过的 session Markdown，并且排除当前会话。

60
00:04:51,712 --> 00:04:56,039
然后是触发事实，这是最容易被讲错的地方。

61
00:04:56,039 --> 00:05:01,952
默认 check_interval_secs 是 None，代表不启用周期检查。

62
00:05:01,952 --> 00:05:06,508
源码明确支持的是会话结束和手动的 dream 命令。

63
00:05:06,508 --> 00:05:12,181
只有在配置了检查间隔之后，session actor 才会按周期去检查门控。

64
00:05:12,181 --> 00:05:17,121
所以不能把 Dream 概括成所有空闲时必然自动运行。

65
00:05:17,121 --> 00:05:18,984
这句话是错的。

66
00:05:18,984 --> 00:05:24,188
默认情况下，它只在会话结束时和手动触发时才跑。

67
00:05:24,188 --> 00:05:26,159
这个区别很重要。

68
00:05:26,159 --> 00:05:36,087
如果你在做容量规划或者成本估算，默认行为下 Dream 的调用次数等于会话结束次数，而不是某种定时器频率。

69
00:05:36,244 --> 00:05:42,347
再看锁、幂等和失败恢复，这块我认为是整套设计最值得学的地方。

70
00:05:42,297 --> 00:05:45,879
DreamLock 是最佳努力协调，不是严格互斥。

71
00:05:45,879 --> 00:05:51,324
锁文件叫 .dream-lock，保存 PID，并且用 mtime 兼作上次成功时间。

72
00:05:51,324 --> 00:05:57,309
活进程持有且未过期的时候，返回 Ok 加 None，也就是放弃本次执行。

73
00:05:57,309 --> 00:06:00,867
死进程或者超时的锁可以被回收。

74
00:06:00,867 --> 00:06:04,425
源码注释明确说明它并非严格互斥。

75
00:06:04,425 --> 00:06:10,723
写后复读能降低竞争概率，但仍然可能有两个进程都认为自己获胜。

76
00:06:10,723 --> 00:06:16,348
所以结论很硬，Dream 必须容忍重复 consolidation，也就是必须幂等。

77
00:06:16,348 --> 00:06:19,172
这是一个很诚实的工程选择。

78
00:06:19,172 --> 00:06:26,095
与其上一把分布式锁增加复杂度和故障面，不如承认竞争存在，把幂等做扎实。

79
00:06:26,095 --> 00:06:31,011
然后是成功边界决定清理边界，这句是本段的重点。

80
00:06:31,011 --> 00:06:38,692
模型返回空、返回 NO_REPLY、或者没有 Markdown 标题的时候，不写入也不删 session。

81
00:06:38,692 --> 00:06:41,805
注意是既不写也不删，保持一致。

82
00:06:41,805 --> 00:06:46,853
写 MEMORY.md 失败的时候，调用 rollback 恢复旧的锁状态。

83
00:06:46,853 --> 00:06:51,336
这样下次还能再试，不会因为一次失败永久卡住。

84
00:06:51,336 --> 00:06:58,187
写入成功之后才清理已读取的 session，而且五分钟内仍然活跃的文件会跳过。

85
00:06:58,187 --> 00:07:02,754
这个五分钟的宽限很实用，避免删掉正在写的文件。

86
00:07:02,754 --> 00:07:04,341
最后是索引。

87
00:07:04,341 --> 00:07:11,252
搜索索引只移除实际删掉的路径，再为新的 MEMORY.md 重建索引和 embedding。

88
00:07:11,252 --> 00:07:24,581
所以一次 Dream 的可靠性来自四件事，门控减少无效调用，最佳努力锁降低并发，幂等承担少量重复风险，回滚加延迟清理保证失败后仍能再次尝试。

89
00:07:24,724 --> 00:07:28,832
第三个话题，AgentDefinition 与 Persona 如何合并。

90
00:07:28,782 --> 00:07:36,895
子 Agent 先解析可执行骨架，再把 spawn 参数、role 默认值和 Persona 默认值折叠成运行时配置。

91
00:07:36,895 --> 00:07:41,607
两套结构在不同阶段生效，最终共同决定子会话。

92
00:07:41,607 --> 00:07:44,527
四个结构各管什么，先分清。

93
00:07:44,527 --> 00:07:48,097
AgentDefinition 是可版本化的 Agent 合同。

94
00:07:48,097 --> 00:08:03,265
从 grok 目录下 agents 目录里的 Markdown 文件解析，真实字段包括 prompt_mode、tool_config、capability_mode、permission_mode、tools、isolation、model、hooks 和 MCP 继承等。

95
00:08:03,265 --> 00:08:07,039
项目定义的发现优先级高于 user 和 bundled。

96
00:08:07,039 --> 00:08:10,621
SubagentRole 是按类型命中的运行时预设。

97
00:08:10,621 --> 00:08:16,763
role 可以给出 capability、model、reasoning effort、prompt file 和默认 isolation。

98
00:08:16,763 --> 00:08:21,462
它由 subagent_type 查找，role prompt 在 spawn 时读取。

99
00:08:21,462 --> 00:08:24,720
SubagentPersona 是按名称选择的行为层。

100
00:08:24,720 --> 00:08:32,196
Persona 有 inline instructions、instructions file、inputs、outputs、model、reasoning effort 和 default isolation。

101
00:08:32,196 --> 00:08:37,604
inline 文本在文件内容之前合并，再作为一个 persona 块进入 prompt。

102
00:08:37,604 --> 00:08:40,633
EffectiveRuntimeConfig 是已解析结果。

103
00:08:40,633 --> 00:08:56,282
真实字段是 model、reasoning_effort、capability_mode、persona、persona_instructions、role_prompt、role_prompt_warning、role_name、persona_error 和 isolation。

104
00:08:56,282 --> 00:08:58,337
这里有个事实校正。

105
00:08:58,337 --> 00:09:03,518
源码里没有 temperature、max_tokens 和 tools 这三个字段。

106
00:09:03,518 --> 00:09:06,414
别在你的复述里加上它们。

107
00:09:06,556 --> 00:09:10,989
然后是优先级，必须按字段读，不能一概而论。

108
00:09:10,939 --> 00:09:18,956
第一级是 spawn override，也就是调用 task 时显式给出的 model、reasoning、capability、persona、isolation。

109
00:09:18,956 --> 00:09:25,013
第二级是 role default，role 给的 model、reasoning、capability 和 isolation 默认值。

110
00:09:25,013 --> 00:09:30,121
第三级是 persona default，Persona 给的 model、reasoning 和 isolation。

111
00:09:30,121 --> 00:09:35,542
注意第三级里没有 capability，因为 Persona 不提供 capability_mode。

112
00:09:35,542 --> 00:09:37,898
这是个很容易写错的点。

113
00:09:37,898 --> 00:09:40,338
第四级是 parent 或者 none。

114
00:09:40,338 --> 00:09:46,768
未命中的字段保留 None，交由下游继承父级，isolation 最终落到 None 模式。

115
00:09:46,768 --> 00:09:49,340
再往后还有 definition fallback。

116
00:09:49,340 --> 00:09:56,600
shell 收到解析结果后，如果 reasoning_effort 仍然为空，会读 AgentDefinition 的 effort。

117
00:09:56,600 --> 00:10:02,742
如果 runtime isolation 为 None 且 definition isolation 为 Worktree，也会升级为 Worktree。

118
00:10:02,742 --> 00:10:09,881
model 解析里，已解析的 runtime override 优先于 per-agent pin、AgentDefinition 的 model 和父模型继承。

119
00:10:09,881 --> 00:10:13,571
最后是两种失败模式，一个硬一个软。

120
00:10:13,571 --> 00:10:15,818
Persona 是失败关闭。

121
00:10:15,818 --> 00:10:23,427
请求了 Persona 之后，找不到、内容为空或者读取文件失败，都会写入 persona_error。

122
00:10:23,427 --> 00:10:30,278
文件 I/O 失败会提前返回默认化结果，spawn 侧看到 Persona 错误后中止创建。

123
00:10:30,278 --> 00:10:32,333
role prompt 是软降级。

124
00:10:32,333 --> 00:10:42,165
prompt_file 读取失败只产生 role_prompt_warning，其余的 model、reasoning、capability 和 isolation 继续解析。

125
00:10:42,165 --> 00:10:45,193
一个是中止，一个只是警告。

126
00:10:45,193 --> 00:10:49,581
区分这两种，是理解这套配置系统的关键。

127
00:10:49,732 --> 00:10:52,278
收尾，列能带走的。

128
00:10:52,228 --> 00:10:55,809
第一，检索流水线要有降级路径。

129
00:10:55,809 --> 00:11:00,124
embedding 失败退化为纯全文，MMR 默认关闭。

130
00:11:00,124 --> 00:11:04,908
每一步可选增强都要想清楚它挂了之后系统还剩什么。

131
00:11:04,908 --> 00:11:09,043
第二，先宽召回再精排，候选取三倍。

132
00:11:09,043 --> 00:11:13,466
这是检索系统的标准做法，不要一上来就精排。

133
00:11:13,466 --> 00:11:16,374
第三，合并分数要设下限。

134
00:11:16,374 --> 00:11:22,192
双路合并时不低于原本的 FTS 分数，保证增强不会让结果变差。

135
00:11:22,192 --> 00:11:25,581
这句看似简单，很多系统做不到。

136
00:11:25,581 --> 00:11:28,790
第四，后台整理任务要有门控。

137
00:11:28,790 --> 00:11:34,379
配置门、时间门、会话门，三道门把无效调用挡在前面。

138
00:11:34,379 --> 00:11:40,124
而且要准确描述触发条件，不能说成空闲时必然自动运行。

139
00:11:40,124 --> 00:11:44,619
第五，最佳努力锁配幂等，比强行互斥划算。

140
00:11:44,619 --> 00:11:50,821
承认竞争存在，把重复执行的容错做扎实，系统更简单也更稳。

141
00:11:50,821 --> 00:11:54,103
第六，成功边界决定清理边界。

142
00:11:54,103 --> 00:12:01,531
写失败要回滚锁状态，清理只在写入成功之后，还要给活跃文件留宽限时间。

143
00:12:01,531 --> 00:12:06,002
第七，配置合并是逐字段级联，不是整体覆盖。

144
00:12:06,002 --> 00:12:10,773
先确认这个字段真实存在于哪个结构，再谈优先级。

145
00:12:10,773 --> 00:12:17,708
Persona 没有 capability_mode，EffectiveRuntimeConfig 没有 temperature 和 max_tokens。

146
00:12:17,708 --> 00:12:19,499
最后留一个问题。

147
00:12:19,499 --> 00:12:23,754
你的系统里有没有那种后台定时整理的任务。

148
00:12:23,754 --> 00:12:31,158
如果有，去看看它的门控条件是不是真的写对了，以及它失败一次之后还能不能自动重试。

149
00:12:31,158 --> 00:12:35,040
这两个地方出问题的概率，比主流程高得多。

150
00:12:35,040 --> 00:12:38,430
本集到此，我们下一集继续往下拆。

