dsh-memory-tiers 0.5.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2460 -1679
- package/README.md +268 -206
- package/client.mjs +608 -425
- package/compaction.mjs +50 -43
- package/dshllm.mjs +141 -0
- package/index.mjs +826 -670
- package/memory5.mjs +259 -42
- package/memory6.mjs +1553 -0
- package/package.json +63 -58
- package/reindex.mjs +3 -3
- package/settings.mjs +327 -241
- package/silicon.mjs +56 -8
package/ARCHITECTURE.md
CHANGED
|
@@ -1,1679 +1,2460 @@
|
|
|
1
|
-
# 六层记忆 · 系统架构
|
|
2
|
-
|
|
3
|
-
> 权威设计文档。改动前先读这里,改完同步这里。
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
|
89
|
-
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
###
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
| `
|
|
241
|
-
| `
|
|
242
|
-
| `
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
| `
|
|
254
|
-
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
L2
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
2.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
|
487
|
-
|
|
|
488
|
-
|
|
|
489
|
-
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
###
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
|
566
|
-
|
|
|
567
|
-
|
|
|
568
|
-
|
|
|
569
|
-
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
###
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
| `
|
|
643
|
-
| `
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
|
700
|
-
|
|
701
|
-
|
|
|
702
|
-
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
###
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
##
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
`
|
|
847
|
-
|
|
848
|
-
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
|
896
|
-
|
|
897
|
-
|
|
|
898
|
-
|
|
|
899
|
-
|
|
|
900
|
-
|
|
|
901
|
-
|
|
|
902
|
-
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
|
911
|
-
|
|
912
|
-
|
|
|
913
|
-
|
|
|
914
|
-
|
|
|
915
|
-
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
3
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
```
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
###
|
|
1164
|
-
|
|
1165
|
-
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
```
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
```
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
```
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
```
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
###
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1
|
+
# 六层记忆 · 系统架构
|
|
2
|
+
|
|
3
|
+
> 权威设计文档。改动前先读这里,改完同步这里。
|
|
4
|
+
> 最后更新:**2026-11-02**
|
|
5
|
+
>
|
|
6
|
+
> **这是项目唯一的设计文档**。其他过程性文档(`REFACTOR_PLAN.md` / `SHARED_MEMORY_PLAN.md` /
|
|
7
|
+
> `README4.md` / `SIMULATION.md` / `CHANGES.md` / `sim90-report.md`)已于 2026-11-02 删除,
|
|
8
|
+
> 有用的内容并入本文;旧版本仍可从 git 历史取回(`git log --diff-filter=D --name-only`)。
|
|
9
|
+
> 本文只讲「现在是什么样」和「接下来要改成什么样」;待办与决策集中在第 27 节。
|
|
10
|
+
>
|
|
11
|
+
> **注意**:第 6 节是 2026-10-31 已落地的改动(含实测数据)。其余各节描述的是当前实际状态。
|
|
12
|
+
> 2026-11-01 的改动集中在**分区 D**(第 20–24 节)。
|
|
13
|
+
>
|
|
14
|
+
> **分区**:A 设计与机制(0–8)· B 问题与预备方案(9–14)· C dsh 接入(15–19)· D 插件化落地(20–24)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 怎么读这份文档 · 新内容写在哪
|
|
23
|
+
|
|
24
|
+
本文档按**主题**分区,不是按时间堆的。动手前先判断你要改的东西属于哪个区,
|
|
25
|
+
写进那一区;**不要**一律 append 到文件末尾——那正是这份文档一度变乱的原因。
|
|
26
|
+
|
|
27
|
+
| 分区 | 节 | 讲什么 | 什么时候动它 |
|
|
28
|
+
| --------------- | ----- | ----------------------------------------- | --------------------- |
|
|
29
|
+
| **A · 设计与机制** | 0–8 | 六层结构、完整流水线、模型调用矩阵、指标与热度、关键参数、对外接口、三条不能碰的线 | 改分层逻辑、容量、热度公式、接口语义 |
|
|
30
|
+
| **B · 问题与预备方案** | 9–14、25 | 现状与已知问题、未实施的预备方案(向量索引改造、图片记忆)、文件结构、提示词清单 | 记坑、记「想做但还没做」的东西 |
|
|
31
|
+
| **C · dsh 接入** | 15–19 | 上下文怎么接、历史压缩、token 成本实测、dsh 适配层速查 | 换 agent、改注入方式、排查上下文成本 |
|
|
32
|
+
| **D · 插件化落地** | 20–24 | 设置页、模型分阶段配置、向量指纹与重建 | 改界面、改模型配置、动向量库 |
|
|
33
|
+
|
|
34
|
+
**写新内容的规矩**:
|
|
35
|
+
|
|
36
|
+
1. 先按上表定位分区;落在已有主题里就**改那一节**,不要新开一节
|
|
37
|
+
2. 确实是新主题,才在该分区末尾**追加新节**并取下一个编号(目前最大 24)
|
|
38
|
+
3. 改完顺手在下面「变更索引」**顶部**加一行
|
|
39
|
+
|
|
40
|
+
### 变更索引(倒序)
|
|
41
|
+
|
|
42
|
+
| 版本 | 日期 | 改了什么 | 节 |
|
|
43
|
+
| ------ | ----- | ----------------------------------------------------------------- | ------- |
|
|
44
|
+
| — | 11-02 | **真实模型仿真**:新增 `sim-real.mjs`(9 组、24 项断言);修 `memory6` 缺 `update` 分支(模型判"进展"时既不记 `updates` 也不打日志,看起来像融合没生效);判关系提示词补充"重复强调同一规则也算 duplicate"。结论见 27.6 | 26、27.6 |
|
|
45
|
+
| — | 11-02 | **只读层缓存**:`search`/`buildBlock`/`stats`/`list` 走 `readLayerCached`(mtime+size 失效),`detail` 的 sources 下探改为查缓存索引;三道上限防内存不足。10 万条检索 1.4 s → 91 ms | 27.7 |
|
|
46
|
+
| v0.8.0 | 11-01 | **六文件布局**:`memory6.mjs` 取代 memory5 做默认。L1 = 真文件 + 每端 temp;谁先结束谁拿钥匙整理;溢出按整轮取;L1 记问+答;DL1 与 L2 旧半一起处理;L3 独立快照;检索按层读文件并顺着 sources 下探 DL2。旧数据不迁移,`layout: "legacy"` 可回退 | 26 |
|
|
47
|
+
| v0.7.0 | 11-01 | **融合的边界**:跨写者不合并/不取代(`fuseDL3` 候选、搬层、连带作废、热度四处跳过外来条目)。修掉一个会**丢数据**的洞:跨文件判重会删掉自己的新摘要、把内容并到对方对象上(存不回去)。注入侧:自己的排前面,别人的条目只借剩余空间 | 13 |
|
|
48
|
+
| v0.7.0 | 11-01 | **多写者共享目录**:存储按写者分片(各写各的 `store-<writer>.json`,读时合并),改数据只动自己的;目录可用指针文件 `~/.dsh/memory-tiers.json` 任意指定 | 13 |
|
|
49
|
+
| — | 11-01 | **文档二次校正**:清掉 `injectHot` 的全量残留(它已删除、三层恒定注入);第 18 节默认改为**完全取替**;第 21 节默认模型改为 BAAI 那对;README 同步 | 16、18、20、21 |
|
|
50
|
+
| v0.6.0 | 11-01 | **刷新机制改造**:`refreshMinMs` 从「节流整个记忆块」改为只节流 **L3 视图(DL3 排序)**,L1/L2 随写随带;移除 `injectHot` 开关(三层恒定注入);历史压缩退出设置页(默认完全取替);API Key 挪到嵌入与重排下 | 16、20 |
|
|
51
|
+
| — | 11-01 | **文档整理**:按主题分 A–D 四个区并加可见分界;删除重复的第 23 节;README 同步(版本号、文件清单、过时断言) | 全文 |
|
|
52
|
+
| v0.6.0 | 11-01 | 新库默认 `compactKeepTurns: 0`;清掉本地哈希向量在注释和文案里的残留 | 16、23 |
|
|
53
|
+
| v0.6.0 | 11-01 | 模型配置改版:`preset` 预设 + 融合/摘要可跟随**当前会话模型**;设置页顶部加每层条数/字数 | 24 |
|
|
54
|
+
| v0.5.2 | 11-01 | **删除本地哈希向量**。无模型时语义层整体关闭(不读不写向量),宁可没有也不假装有 | 23 |
|
|
55
|
+
| v0.5.1 | 11-01 | 向量指纹 `vectors.meta.json` + `reindex.mjs`:同维度换 embed 模型会静默污染 | 22 |
|
|
56
|
+
| v0.5.0 | 11-01 | 四个模型阶段(embed/rerank/chat/summary)可各自指定 base / apiKey / model | 21 |
|
|
57
|
+
| — | 11-01 | `compactKeepTurns: 0` 改为显式支持(后来成为默认);更正「L1 每轮变会打掉缓存」这条过期结论 → 见第 16 节「记忆刷新」 | 16、18 |
|
|
58
|
+
| v0.4.0 | 11-01 | 设置页(设置 → 记忆):12 字段 5 分组 + 只读运行状态 | 20 |
|
|
59
|
+
|
|
60
|
+
> 2026-10-31 及更早的改动按主题散落在第 6、12、18 节(均含实测数据),不在此表重复。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
> **分区 A · 设计与机制**(0–8 节)
|
|
65
|
+
|
|
66
|
+
## 0. 一页速览
|
|
67
|
+
|
|
68
|
+
**六层**,按「离上下文多远」分两组:
|
|
69
|
+
|
|
70
|
+
| 活跃层(进上下文) | 深层(不进上下文,供检索) |
|
|
71
|
+
| ----------------------- | ------------------------ |
|
|
72
|
+
| L1 及时层 · L2 缓冲层 · L3 视图 | DL1 流水 · DL2 原文 · DL3 摘要 |
|
|
73
|
+
|
|
74
|
+
**核心原则(不能碰)**:
|
|
75
|
+
|
|
76
|
+
1. **重复度是重要性信号**。同样的话说了 2 次 vs 10 次,模型压摘要时分得出
|
|
77
|
+
「反复叮嘱」和「刷屏」。提前去重会把这信号抹掉——所以写入不判重、不去重。
|
|
78
|
+
2. **原文时间戳不得变**(`createdAt` 一经写入不再修改),否则遗忘曲线算不准。
|
|
79
|
+
3. **大模型只做摘要,小模型只做判关系**,其余全是本地规则或不处理。
|
|
80
|
+
4. **搜到 ≠ 相关**。检索是只读的,热度要 `attend()` 确认后才涨,否则形成回音室。
|
|
81
|
+
5. **摘要进 DL3 是稀有的**,大部分内容只配当原文沉到 L2 / DL1。
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 1. 六层结构
|
|
86
|
+
|
|
87
|
+
| 层 | 性质 | 存什么 | 进上下文 | 容量 | 遗忘 |
|
|
88
|
+
| ---------- | -- | --------------------- | ----------- | -------------- | --------------- |
|
|
89
|
+
| **L1** 及时层 | 活跃 | 刚写入的**原文** | ✓ 全进 | 1500 字 / 200 条 | 不遗忘(靠溢出清) |
|
|
90
|
+
| **L2** 缓冲层 | 活跃 | **原文**(比 L1 重要、不那么及时) | ✓ 全进 | 3000 字 / 120 条 | 不遗忘(靠满仓清) |
|
|
91
|
+
| **L3** 视图 | 活跃 | DL3 的**排序前 N 条** | ✓ | 1500 字 | 不遗忘(自动让位) |
|
|
92
|
+
| **DL1** 流水 | 深层 | **原文**,没被摘要用上的 | ✗ | 无上限 | **快忘**(半衰期 3 天) |
|
|
93
|
+
| **DL2** 原文 | 深层 | **原文**,被 ★ 摘要引用的 | ✗ | 60000 字 | **不遗忘**(保底) |
|
|
94
|
+
| **DL3** 摘要 | 深层 | **摘要**(长期记忆) | ✗(L3 是它的视图) | 无上限 | **不遗忘** |
|
|
95
|
+
|
|
96
|
+
**关键关系**:
|
|
97
|
+
|
|
98
|
+
- L3 **不是存储层**,是「从 DL3 里按分数截取出来的视图」。DL3 里热度掉了,
|
|
99
|
+
分数就掉,自动退出上下文,不需要搬数据。
|
|
100
|
+
- DL2 的内容**一定有对应的 DL3 摘要**(是摘要引用它才进 DL2 的)。
|
|
101
|
+
所以 DL3 找不到时,DL2 也不会有——检索可以跳过 DL2 直奔 DL1。
|
|
102
|
+
- DL1 是「没被摘要用上的流水账」,DL2 是「被长期记忆收录的原文」。
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 2. 完整流水线
|
|
107
|
+
|
|
108
|
+
以 10 条消息为例。
|
|
109
|
+
|
|
110
|
+
### ① 写入(10 条 → L1)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
做什么:算向量 + 落盘
|
|
114
|
+
不做:判重、分类、合并、摘要
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**全部原样进 L1**。理由见原则 1。
|
|
118
|
+
|
|
119
|
+
成本:1 次批量 embed。
|
|
120
|
+
|
|
121
|
+
### ② L1 满了 → 取最老一半
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
做什么:按写入时间取一半(如 5 条)
|
|
125
|
+
判断:本地规则,不调模型
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### ③ 溢出处理(这 5 条)—— **唯一的大模型调用**
|
|
129
|
+
|
|
130
|
+
一次调用,输入 5 条原文,输出三段:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
★ 星鲸定下以后报告统一用 markdown《1,3,5》 ← 特别重要 → DL3,1/3/5 → DL2
|
|
134
|
+
· 封魔说快递柜放错跑了两趟《4》 ← 一般 → 不进 DL3,4 → L2/DL1
|
|
135
|
+
+ 2 ← 值得留但不特别 → L2
|
|
136
|
+
(没提到的 → DL1)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
路由规则:
|
|
140
|
+
|
|
141
|
+
| 标记 | 摘要去哪 | 引用的原文去哪 |
|
|
142
|
+
| --- | ------- | ---------- |
|
|
143
|
+
| `★` | **DL3** | **DL2** |
|
|
144
|
+
| `·` | 不进 DL3 | 跟着走 L2/DL1 |
|
|
145
|
+
| `+` | — | **L2** |
|
|
146
|
+
| 都没提 | — | **DL1** |
|
|
147
|
+
|
|
148
|
+
> **没进 DL3 的摘要,其引用的原文不能进 DL2**——DL2 的职责是「供下探查细节」,
|
|
149
|
+
> 而下探靠 DL3 的摘要引过去。没有摘要指向它,DL2 就是孤儿,永远搜不到。
|
|
150
|
+
|
|
151
|
+
优先级:`★` > `+` > 无。同一条编号被两个标记命中时,以 `★` 为准。
|
|
152
|
+
|
|
153
|
+
> **L1 路径和 L2 路径的差异**(2026-10-31 用户定):
|
|
154
|
+
> L1 溢出(第一批沉淀)**只收 `★`** 进 DL3,`·` 被过滤、其 sources 跟着走 L2/DL1;
|
|
155
|
+
> L2 溢出(缓冲区整理)**不设限**,`★` 和 `·` 都正常进 DL3。
|
|
156
|
+
> 实现见 `routeByCitation` 的 `ROUTE_GATE` 分支。
|
|
157
|
+
|
|
158
|
+
### ④ DL3 融合(新摘要入 DL3 时)—— **唯一的小模型调用**
|
|
159
|
+
|
|
160
|
+
新摘要进 DL3 前,先搜 DL3 现有摘要(余弦 ≥ 0.82),有候选才调模型判:
|
|
161
|
+
|
|
162
|
+
| 判定 | 处理 | fusionCount |
|
|
163
|
+
| ------ | ------------------ | ----------- |
|
|
164
|
+
| **重复** | 合并进旧的,删新的 | 旧的 +1 |
|
|
165
|
+
| **矛盾** | 新取代旧,**热度/计数转给新的** | **不 +1** |
|
|
166
|
+
| **更新** | 两条都留 | — |
|
|
167
|
+
| **无关** | 都留 | — |
|
|
168
|
+
|
|
169
|
+
矛盾时连带作废 DL2 里对应的原文——摘要记了 `sources`,反查就知道该作废哪几条。
|
|
170
|
+
|
|
171
|
+
**融合只在入库时做**,不做定期全量融合。
|
|
172
|
+
|
|
173
|
+
### ⑤ L2 满了 → 整层走一遍 ③
|
|
174
|
+
|
|
175
|
+
L2 是缓冲区,**不设到期**,超容量(3000 字 / 120 条)才触发。
|
|
176
|
+
|
|
177
|
+
取法(2026-10-31 定):
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
L2 按 consolidatedAt(进 L2 的时间)排序
|
|
181
|
+
→ 从最旧起累加字数,到 consolidateMaxChars / 2 为止(最后一条可超出)
|
|
182
|
+
→ 再加上 DL1 的全部
|
|
183
|
+
→ 一起送去走 ③
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
区别只有一处:模型标 `+` 的这次直接沉 DL2(观察期用完了)。
|
|
187
|
+
|
|
188
|
+
> **为什么用 `consolidatedAt` 而不是 `createdAt` 排序**:DL1 升回 L2 的条目
|
|
189
|
+
> 内容时间可能很老,用 `createdAt` 排会把它排到最前,打乱时间流(缓存)。
|
|
190
|
+
> 用「进 L2 的时间」排,它就自然融入当前时间窗。
|
|
191
|
+
> 原文 `createdAt` 本身不变(见原则 2),只是排序键不同。
|
|
192
|
+
|
|
193
|
+
### ⑥ 检索 → 关注
|
|
194
|
+
|
|
195
|
+
三档,**只能按顺序下探**:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
summary 只扫 DL3 ← 只要大意
|
|
199
|
+
detail 扫 DL3 → DL2 ← 要细节
|
|
200
|
+
full 扫 DL3 → DL2 → DL1 ← 确实没有才下到底
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
会话模型选档。结果带 `confidence`(rerank 原始分)+ `advice`(下一步建议)。
|
|
204
|
+
|
|
205
|
+
**搜到不等于涨热度**。要会话模型调 `attend(ids)` 确认相关,才 `recallCount +1`,
|
|
206
|
+
DL1 的升回 L2 重新观察。被搜到但模型判断无关的,不涨。
|
|
207
|
+
|
|
208
|
+
相关性闸门用 **rerank 原始分**(`minRerank: 0.02`),不用余弦——
|
|
209
|
+
实测余弦分不开(不相关 0.573 vs 相关 0.690),reranker 差两个数量级。
|
|
210
|
+
|
|
211
|
+
### ⑦ 遗忘
|
|
212
|
+
|
|
213
|
+
只扫 **DL1**,按保持率衰减,低于阈值进台账后删。**DL3 不遗忘,DL2 不遗忘**。
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 3. 模型调用矩阵
|
|
218
|
+
|
|
219
|
+
| 阶段 | 本地规则 | 小模型 Ling-mini | 大模型 DeepSeek |
|
|
220
|
+
| -------- | ----------- | -------------- | ---------------- |
|
|
221
|
+
| ① 写入 | 落盘 | embed 向量 | — |
|
|
222
|
+
| ② L1 满 | 取最老一半 | — | — |
|
|
223
|
+
| ③ 溢出 | 按出处路由 | — | **摘要 + 出处 + 标记** |
|
|
224
|
+
| ④ 入 DL3 | 合并/取代执行 | **判重复/矛盾/更新** | — |
|
|
225
|
+
| ⑤ L2 满 | 取旧半 + DL1 全 | — | 同 ③ |
|
|
226
|
+
| ⑥ 检索 | 粗排打分 | embed + rerank | — |
|
|
227
|
+
| ⑦ attend | 记热度 | — | — |
|
|
228
|
+
| ⑧ 遗忘 | 衰减 + 台账 | — | — |
|
|
229
|
+
|
|
230
|
+
**大模型只在 ③⑤ 出现,小模型只在 ①④⑥ 出现。** 其余全是本地规则或不处理。
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 4. 指标与热度
|
|
235
|
+
|
|
236
|
+
### 三个指标,语义不同
|
|
237
|
+
|
|
238
|
+
| 指标 | 含义 | 什么时候涨 |
|
|
239
|
+
| ------------- | --------------- | ---------------------------------- |
|
|
240
|
+
| `fusionCount` | **融合次数 / 重复次数** | 同一件事被反复说、被合并掉几次。DL3 融合判「重复」时 +1 |
|
|
241
|
+
| `recallCount` | **关注次数** | 被会话模型**确认相关**后用过几次。`attend()` 时 +1 |
|
|
242
|
+
| `heat` | **热度** | 见下表 |
|
|
243
|
+
|
|
244
|
+
### 热度来源(2026-10-31 定)
|
|
245
|
+
|
|
246
|
+
| 事件 | heat 变化 |
|
|
247
|
+
| ------------- | --------------- |
|
|
248
|
+
| 写入 | **不涨** |
|
|
249
|
+
| 摘要入 DL3(初始) | **= 2**(新鲜度) |
|
|
250
|
+
| 融合判「重复」合并进旧的 | 旧的 +1 |
|
|
251
|
+
| 融合判「矛盾」 | **不 +1**,热度转给新的 |
|
|
252
|
+
| 融合判「无关」 | 不涨 |
|
|
253
|
+
| `attend` 确认相关 | +1 |
|
|
254
|
+
| 搜到但模型说无关 | **不涨** |
|
|
255
|
+
|
|
256
|
+
### 热度公式
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
半衰期 halfLife = heatHalfLife × (1 + heatRepeatBoost × min(repeatStrength, heatRepeatCap))
|
|
260
|
+
热度 heat(t) = heat × exp(-t / halfLife)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- `repeatStrength`:被提及的天数累积(艾宾浩斯重复效应)。天数越多,半衰期越长。
|
|
264
|
+
- `touchHeat` **按天去重**:同一天多次提及只算一次。
|
|
265
|
+
- 衰减是指数的,被提及过的衰减得更慢。
|
|
266
|
+
|
|
267
|
+
### L3 排序
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
score = importance × heat
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
按 score 降序,累加字数填满 `longMaxChars`(1500 字),可略微超出。
|
|
274
|
+
|
|
275
|
+
- `importance`:模型给档位,0.9(重要)/ 0.7(一般)/ 0.5(琐事)
|
|
276
|
+
- `heat`:范围 1~20,天然主导排序(档位影响小)
|
|
277
|
+
|
|
278
|
+
**待验证**:跑 L3 前 20 条的档位分布。若第一档仍占 90%,改成
|
|
279
|
+
`importance^0.5 × heat` 进一步压缩档位影响。**不做硬配额**。
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 5. 关键参数
|
|
284
|
+
|
|
285
|
+
| 参数 | 默认 | 说明 |
|
|
286
|
+
| ---------------------- | ------------ | ------------------------------------------------------------------ |
|
|
287
|
+
| `hotMaxChars` | 2000 | L1 容量(字)。**同时就是注入量**,没有独立上限 |
|
|
288
|
+
| `hotMaxItems` | 200 | L1 容量(条) |
|
|
289
|
+
| `consolidateMaxChars` | 2500 | L2 容量(字) |
|
|
290
|
+
| `consolidateMaxItems` | 120 | L2 容量(条) |
|
|
291
|
+
| `consolidateDays` | 0 | **已弃用**(L2 不按时间到期) |
|
|
292
|
+
| `longMaxChars` | 2000 | L3 视图容量(字)。新库模板覆盖成 4000(`config.json` 的 `memoryCfg`) |
|
|
293
|
+
| `dl2MaxChars` | 12000 | DL2 容量 |
|
|
294
|
+
| `dl2CompressTarget` | 400 | 单条超这么多字才值得压缩 |
|
|
295
|
+
| `allowModelCompress` | false | 模型压缩默认关闭(实测减不掉字) |
|
|
296
|
+
| `routeMode` | `citation` | 路由依据:摘要引用,不用重要度数值 |
|
|
297
|
+
| `classifyMode` | `onOverflow` | 分类挪到溢出后(现已被 citation 取代) |
|
|
298
|
+
| `summarizeMode` | `inline` | `deferred` = 后台队列,不卡写入 |
|
|
299
|
+
| `summaryChunk` | 60 | 摘要单次最多压多少条 |
|
|
300
|
+
| `fusionPrefilter` | 0.82 | 判关系的余弦预筛 |
|
|
301
|
+
| `minRerank` | 0.02 | 检索相关性硬底(滤纯噪音) |
|
|
302
|
+
| `minRerankStrong` | 0.08 | 「够用」分界(只给建议,不过滤) |
|
|
303
|
+
| `summaryCoherenceWarn` | 0.30 | 摘要「硬凑」检测阈值 |
|
|
304
|
+
| `heatHalfLife` | 10 | 半衰期基准(天)。**不要单独调它来加速 DL1 遗忘**——它同时决定 L3 排序的热度,改了会让热度公式自测全翻 |
|
|
305
|
+
| `forgetThreshold` | 0.25 | **DL1 忘得快靠这个**。保持率低于它就删。heatHalfLife=10 时 0.03→约 35 天忘,0.25→约 14 天 |
|
|
306
|
+
| `heatRepeatBoost` | 0.3 | 重复系数 |
|
|
307
|
+
| `heatRepeatCap` | 20 | 强度封顶 |
|
|
308
|
+
| `heatPromoteThreshold` | 1.0 | 热度 ≥ 这个 → 有资格升长久层 |
|
|
309
|
+
| `budgetTokens` | 0 | 进程级 token 预算,0 = 不限 |
|
|
310
|
+
| `viewRefreshMs` | 1800000 | **L3 视图(DL3 排序)的重算间隔**。全局唯一一处节流,见第 16 节「记忆刷新」;改 `longMaxChars` 会立刻重算 |
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 6. 已实施的改动(2026-10-31 全部落地)
|
|
315
|
+
|
|
316
|
+
| # | 改什么 | 做法 | 实测 |
|
|
317
|
+
| ----- | ------------- | --------------------------------------------------------------------------- | ------------------------------ |
|
|
318
|
+
| **1** | L2 溢出取法 | 按 `consolidatedAt` 排序取旧半(**字数**累加到 `consolidateMaxChars/2`,可超出)+ **DL1 全部** | `CONSOLIDATE_FULL 取旧 N 条(M 字)` |
|
|
319
|
+
| **2** | 摘要进 DL3 收紧 | `★` 进 DL3、`·` 也进但 L1 路径只收 `★`;只有进 DL3 的 sources 才进 DL2 | DL3 从 203 → **10 条** |
|
|
320
|
+
| **3** | importance 三档 | 模型给 `★`→0.9 / `·`→0.7;**热度主导**(heat 1~20 vs 档位 1.29 倍) | 两档都产出 |
|
|
321
|
+
| **4** | 遗忘范围 | **只扫 DL1**;忘得快靠 `forgetThreshold` 0.03→**0.25**(约 14 天忘) | 热度自测 17/17 |
|
|
322
|
+
| **5** | 热度来源 | 新摘要初始 `heat=2`;涨的只有**融合 +1** 和 **attend +1** | — |
|
|
323
|
+
| **6** | 全量融合 | `fusionEveryNWrites: 0`(停用),只靠入库融合 | 判重调用 844→**153** |
|
|
324
|
+
|
|
325
|
+
### ④ 的实际做法(与原计划不同,用户已确认)
|
|
326
|
+
|
|
327
|
+
原计划 `heatHalfLife` 10→3 天,**实测会打翻热度公式**——`heatHalfLife` 同时决定
|
|
328
|
+
L3 排序的热度和 DL1 的保持率,改了之后 7 项热度自测全失败(持续提及的也晋升不了)。
|
|
329
|
+
|
|
330
|
+
改成:**热度公式不动(`heatHalfLife: 10`)**,改用 `forgetThreshold: 0.25`
|
|
331
|
+
(保持率低于它就删)。效果是 DL1 约 14 天忘(原 35 天),快 2.5 倍,
|
|
332
|
+
且热度公式 17/17 自测全过。**用户确认「遗忘架构就现在这样,不用改了」。**
|
|
333
|
+
|
|
334
|
+
### 额外做的一条:L1 / L2 路由差异(用户 14:08 补充)
|
|
335
|
+
|
|
336
|
+
> 「只有 L1 进 DL3 这条路需要特重要事件的条件,L2 进 DL3 不需要限制太大,就按正常的」
|
|
337
|
+
|
|
338
|
+
在 `routeByCitation` 加了 `ROUTE_GATE` 分支:
|
|
339
|
+
|
|
340
|
+
- **L1 路径**(L1 溢出):只收 `★`(importance ≥ 0.9)进 DL3,
|
|
341
|
+
`·` 被过滤、其 sources 跟着走 L2/DL1
|
|
342
|
+
- **L2 路径**(`secondChance`):`★` 和 `·` 都正常进 DL3
|
|
343
|
+
|
|
344
|
+
理由:L1 溢出是第一批沉淀,DL3 是稀有长期记忆要挑最值得的;
|
|
345
|
+
L2 满了做整理,本来就是"正常摘要",不该更严。
|
|
346
|
+
|
|
347
|
+
### 60 天实测(改前 → 改后)
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
总 token 285,695 → 196,940 −31%
|
|
351
|
+
判重 166,020 → 31,438 −81%
|
|
352
|
+
调用总数 1080 → 419 −61%
|
|
353
|
+
写入耗时 111s → 66s −40%
|
|
354
|
+
健康检查 11/11 全过
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 档位分布(用户关心「别全是第一档」)
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
L1 路径(严格) → 只产 ★ = 0.9
|
|
361
|
+
L2 路径(正常) → 产 ★ = 0.9 和 · = 0.7
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
30 天 L2 只攒到 1608 字(上限 3000)没触发;调小到 500 字验证到 L2 路径确实产 0.7。
|
|
365
|
+
**正常容量下 L2 约 50-60 天满一次**,所以 0.7 是慢热的。不是全第一档 ✓
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## 7. 对外接口
|
|
370
|
+
|
|
371
|
+
```js
|
|
372
|
+
const mem = createMemory5({
|
|
373
|
+
dir, now, embed, classify, classifyBatch,
|
|
374
|
+
judgeRelation, summarize, summarizeWithSources, rerank, compress,
|
|
375
|
+
cfg: {...},
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
// 写
|
|
379
|
+
await mem.write({ text, speaker, people, scene, channel, importance, at, kind })
|
|
380
|
+
await mem.writeMany([{...}, {...}]) // 批量,快得多
|
|
381
|
+
|
|
382
|
+
// 查
|
|
383
|
+
await mem.search(q, { depth, limit, person, channel, useRerank })
|
|
384
|
+
// depth: 'auto' | 'summary' | 'detail' | 'full'
|
|
385
|
+
// 返回 { hits, confidence, advice, depth, stages }
|
|
386
|
+
mem.attend(ids) // 确认相关,记关注
|
|
387
|
+
mem.buildBlock(budget) // 注入上下文
|
|
388
|
+
mem.list(layer?) // 列条目
|
|
389
|
+
|
|
390
|
+
// 维护
|
|
391
|
+
mem.sweep() // 触发分层收敛(enforceLimits)
|
|
392
|
+
mem.compressDL2({ force, only }) // 手动压缩 DL2
|
|
393
|
+
mem.flushSummaries() // 清摘要队列
|
|
394
|
+
mem.pendingSummaries() // 队列里还有多少
|
|
395
|
+
mem.fuse() // 全量融合(待停用)
|
|
396
|
+
mem.fuseDL3(summaryItem, now) // 单条入 DL3 融合
|
|
397
|
+
|
|
398
|
+
// 观测
|
|
399
|
+
mem.stats() // 分层统计 + 调用统计 + 闸门
|
|
400
|
+
mem.resetCalls()
|
|
401
|
+
mem.graveyard({ limit }) // 遗忘台账
|
|
402
|
+
|
|
403
|
+
// 测试
|
|
404
|
+
mem._seed(item, vec) // 直接注入
|
|
405
|
+
mem.retentionOf / heatOf / halfLifeOf
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 8. 三条不能碰的线
|
|
413
|
+
|
|
414
|
+
1. **不写入判重**。重复度是重要性信号,提前合并会抹掉它。
|
|
415
|
+
(历史上曾有「写入时判重」,60 天烧掉 844 次调用 / 16.6 万 token / 58%,已废除。)
|
|
416
|
+
2. **原文时间戳不得变**。`createdAt` 一旦写入不再改,否则遗忘曲线算不准。
|
|
417
|
+
3. **搜到不涨热度**。只认 `attend()` 确认,否则误命中的记忆越搜越热,成回音室。
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
> **分区 B · 已知问题与预备方案**(9–14 节)
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 9. 现状与已知问题
|
|
425
|
+
|
|
426
|
+
**已解决**(60 天压测 1806 条 / 6 分钟 / 28.6 万 token,11/11 检查通过):
|
|
427
|
+
|
|
428
|
+
- 写入判重占 58% 成本且随库增长 → 已废除
|
|
429
|
+
- 检索每次全量写盘 → 已去掉(400ms → 23ms)
|
|
430
|
+
- 分层检索没真正分层 → 已按层建桶(summary 140ms vs full 548ms)
|
|
431
|
+
- 摘要硬凑 → 加 `SUM_SUSPECT` 审计(实际 1%)
|
|
432
|
+
- 预算撞顶导致检索静默降级 → 加 `RERANK_FAIL` 审计 + `reranked:false` 提示
|
|
433
|
+
|
|
434
|
+
**未解决 / 已知代价**:
|
|
435
|
+
|
|
436
|
+
- ~~向量持久化是单个 JSON,20 万条会 `RangeError`~~ → **已分片(2026-10-31)**:
|
|
437
|
+
每片 5000 条(约 27MB),撑到十万级。再往上(~100 万条)要二进制/偏移索引,见第 12 节。
|
|
438
|
+
- 矛盾检测延后到摘要入 DL3 时(原来在写入时)。旧命令在 L1 里多活 2–4 天,
|
|
439
|
+
但 L1 全进上下文,对话模型看得见先后顺序。
|
|
440
|
+
- 入库融合若漏判(两条摘要余弦没到 0.82),就永远不合并。DL3 不遗忘,
|
|
441
|
+
多几条重复不影响正确性,只是占空间。
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## 10. 预备方案:向量索引改造(**未实施**,2026-10-31 备忘)
|
|
446
|
+
|
|
447
|
+
> **状态:仅预备,不许当已做。** 用户 2026-10-31 定:「只是预备」。
|
|
448
|
+
> 真要动手前先问用户。三条红线(第 8 节)不受本方案任何一项影响。
|
|
449
|
+
|
|
450
|
+
### 背景(实测口径,注意是修正后的数)
|
|
451
|
+
|
|
452
|
+
向量是 **1024 维 Float32Array**(base64 落盘 5.4KB/条)。本地余弦全扫实测 **~2µs/条**:
|
|
453
|
+
|
|
454
|
+
| 库规模 | 全扫一次 | 体感 |
|
|
455
|
+
|---|---|---|
|
|
456
|
+
| 现在(~1200 条) | ~2ms | 无感 |
|
|
457
|
+
| 1 万条 | 22ms | 无感 |
|
|
458
|
+
| 10 万条 | **216ms** | 开始有感 |
|
|
459
|
+
| 100 万条 | 2.2s | 不可接受 |
|
|
460
|
+
|
|
461
|
+
**最先撑不住的是检索**(在应答关键路径上);融合粗筛、写入粗筛在后台,疼得晚。
|
|
462
|
+
写入本地扫描 O(n)/条 和判重 relation O(n)/条 是 90 天模拟(sim90-report.md)确认的两个增长项。
|
|
463
|
+
|
|
464
|
+
### 动手时机
|
|
465
|
+
|
|
466
|
+
- **DL3 过 5000 条**(检索约 0.65s,开始有感)前动手;
|
|
467
|
+
- 10 万条级再改就要停机重建索引 + 换存储,动静大。
|
|
468
|
+
|
|
469
|
+
### 三件事(按序)
|
|
470
|
+
|
|
471
|
+
1. **删除/作废路径索引友好化**(免费,可提前做)
|
|
472
|
+
现在 `removeItem` + `store.vectors.delete` 配对散落多处,`supersededBy` 作废只改字段。
|
|
473
|
+
收拢成统一入口:**能标记失效(tombstone)、能重建**。这一步不引依赖,
|
|
474
|
+
先把数据流理顺,真上 HNSW 时不动调用方。
|
|
475
|
+
2. **channel 分桶用足**(零依赖过渡)
|
|
476
|
+
`buildVectorIndex` 的按 channel 桶是现成的(原写入判重遗留),给
|
|
477
|
+
`searchOnce` 带 channel 过滤的场景用上:同 channel 池内比,省掉全层遍历。
|
|
478
|
+
3. **上 HNSW**(hnswlib-node 或同类)
|
|
479
|
+
检索、融合粗筛、写入粗筛三处从 O(n) 全扫变 O(log n)。召回靠 `ef_search` 调。
|
|
480
|
+
|
|
481
|
+
### 代价(提前记下,别到时才发现)
|
|
482
|
+
|
|
483
|
+
| 代价 | 说明 | 缓解 |
|
|
484
|
+
|---|---|---|
|
|
485
|
+
| **召回变近似** | 可能漏个别相似候选。检索有 rerank 兜底;融合漏合并只是 DL3 涨快点(DL3 不遗忘,不影响正确性) | `ef_search` 调高 |
|
|
486
|
+
| **删除弱** | HNSW 删除是墓碑 + 定期重建(本系统有遗忘/作废,90 天模拟 500+23 条) | 正好靠第 1 件事的失效标记 |
|
|
487
|
+
| **写入变慢** | 插入建图 1~5ms/条(现在 2µs) | 写入在后台,无感 |
|
|
488
|
+
| **依赖** | hnswlib-node 是原生模块,Windows 编译/分发多一层 | 备选纯 JS(vectra 等,慢几倍) |
|
|
489
|
+
| **复杂度** | 索引与 store 一致性、崩溃恢复、测试面变大 | — |
|
|
490
|
+
|
|
491
|
+
### 明确不做(收益 < 代价)
|
|
492
|
+
|
|
493
|
+
- **降维**(1024→更小):精度掉、融合阈值 0.82 得重调、旧向量要重算。1024 维已经不大,省不了多少。
|
|
494
|
+
- **Float32Array 替代 JSON 落盘**:只省 25% 空间(5.4KB→4KB/条),丢 JSON 可读性 + 迁移脚本。等第 9 节的 20 万条 `RangeError` 真撞上再一起做。
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 11. 预备方案:图片记忆(**未实施**,2026-10-31 备忘)
|
|
499
|
+
|
|
500
|
+
> **状态:仅预备,不动手。** 同上节,动手前先问用户。
|
|
501
|
+
> 三条红线(第 8 节)不受影响。
|
|
502
|
+
|
|
503
|
+
### 核心决策:图不进层,只挂层
|
|
504
|
+
|
|
505
|
+
六层的所有机制(摘要、融合、热度、遗忘)都是为**文本**设计的。图片硬塞进去有三个问题:
|
|
506
|
+
|
|
507
|
+
- 图片没法「摘要」,硬压只剩「主人发了张猫图」,信息全丢;
|
|
508
|
+
- 表情包刷屏会把热度系统喂歪(同一个梗发 20 次,heat 爆表);
|
|
509
|
+
- DL3 会被「图片摘要」淹没,挤掉真正的文字记忆。
|
|
510
|
+
|
|
511
|
+
所以图片的正确位置是**原文的附件**:L1 原文带 `media` 引用,摘要里写「主人发了张猫的照片」(文本照走),要看原图时顺引用去附件库取。
|
|
512
|
+
|
|
513
|
+
### 三层分离
|
|
514
|
+
|
|
515
|
+
```
|
|
516
|
+
① 附件库(blob) media/{sha256}.jpg ← 按内容哈希存,天然去重
|
|
517
|
+
② 索引(两路文本) caption(VLM 描述)+ OCR(图内文字)
|
|
518
|
+
③ 挂载点(记忆条目) media: [{ hash, caption, ocr, w, h }]
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**第 ② 层是关键**:每张图生成两份文本——
|
|
522
|
+
- **VLM caption**:语义描述(「一只橘猫趴在键盘上」),走现有 bge-m3 embedding,**文本 query 直接能搜到图**;
|
|
523
|
+
- **OCR 文字**:图里的字(截图、聊天记录、文档照),群聊场景比 caption 还重要。
|
|
524
|
+
|
|
525
|
+
这样 **Phase 1 不需要任何新检索路径**:caption 当普通文本进 L1/L2,参与摘要、融合、rerank,现有管线一行不改。
|
|
526
|
+
|
|
527
|
+
### 比「单纯存指针」多做的四件事
|
|
528
|
+
|
|
529
|
+
| 要点 | 做法 | 为什么 |
|
|
530
|
+
|---|---|---|
|
|
531
|
+
| 内容哈希去重 | `sha256(图片)` 当文件名,重复只存一份 | 群聊表情包大量重复,不去重磁盘会被刷爆 |
|
|
532
|
+
| 引用计数 | 条目引用 hash,删除时减计数,归零才删文件 | 同一张图可能被多条记忆引用,忘一条不能删图 |
|
|
533
|
+
| 向量独立存 | 图片向量单独索引文件/表 | 现有 `vectors.json` 已接近上限(见第 12 节),别混进去 |
|
|
534
|
+
| 表情包降权 | 哈希重复 >3 次或尺寸 <100px:importance 压 0.5、不进 L3 | 否则 L3 视图全是表情包 |
|
|
535
|
+
|
|
536
|
+
### 两期落地
|
|
537
|
+
|
|
538
|
+
- **Phase 1(零新依赖)**:`commitOne` 加 `media` 字段;带图时算哈希 → 存 `media/` → VLM 出 caption + OCR → caption 拼进记忆文本。检索/摘要/融合全复用现有管线。
|
|
539
|
+
- **Phase 2(可选,跨模态搜图)**:加中文 CLIP(ONNX 本地推理免费),图片向量进独立索引,支持「找那张猫图」这类无文字线索的查询。**不进 Phase 1**——caption 已覆盖大部分场景,多一套索引多一份维护。
|
|
540
|
+
|
|
541
|
+
### 代码落点
|
|
542
|
+
|
|
543
|
+
| 位置 | 改什么 |
|
|
544
|
+
|---|---|
|
|
545
|
+
| `makeItem` | 加 `media: []` |
|
|
546
|
+
| `commitOne` / `writeMany` | 入参加 `images`,写盘前算哈希 + 生成 caption/OCR |
|
|
547
|
+
| `forget()` | 删 archive 文件处顺带减引用计数 |
|
|
548
|
+
| `store.json` | 只增字段,不动结构(向后兼容) |
|
|
549
|
+
| 归档 `.md` | frontmatter 加 `media: [hash...]` |
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## 12. 向量存储扩容(**① 已实施 2026-10-31**;②③ 仍预备)
|
|
554
|
+
|
|
555
|
+
> 解决第 9 节那条 `RangeError: Invalid string length`。
|
|
556
|
+
> **① 分片已落地**(见下方「实施记录」);②③ 仍是不许当已做的预备,动手前问用户。
|
|
557
|
+
> 本节与第 10 节(HNSW 索引)是**两件事**:本节解决「存得下、启动得了」,第 10 节解决「查得快」。内存问题见文末。
|
|
558
|
+
|
|
559
|
+
### 问题定量
|
|
560
|
+
|
|
561
|
+
向量 base64 落盘 **5.4KB/条**(1024 维 Float32Array):
|
|
562
|
+
|
|
563
|
+
| 条数 | vectors.json 大小 | 状态 |
|
|
564
|
+
|---|---|---|
|
|
565
|
+
| 1,184(现在) | 6.5MB | 无感 |
|
|
566
|
+
| 1 万 | 54MB | 正常 |
|
|
567
|
+
| 5 万 | 270MB | parse 变慢(秒级) |
|
|
568
|
+
| **10 万** | **540MB** | **接近 V8 字符串上限,危险** |
|
|
569
|
+
| **20 万** | **1.08GB** | **必炸** |
|
|
570
|
+
|
|
571
|
+
两个原因叠加:① V8 的 JSON 字符串长度上限;② `JSON.parse` 峰值内存是文件的 2~3 倍。
|
|
572
|
+
|
|
573
|
+
### 三层解法(按规模递进)
|
|
574
|
+
|
|
575
|
+
| 阶段 | 做法 | 能撑到 | 改动 | 代价 |
|
|
576
|
+
|---|---|---|---|---|
|
|
577
|
+
| **① 分片 JSON** ✅ | `vectors-part-{i}.json`,每片 ≤5000 条(≤27MB) | ~10 万条 | 约 20 行,当天可上 | 启动要读全部片(5~8 秒) |
|
|
578
|
+
| **② 二进制 + 偏移索引**(治本) | `vectors.bin` 定长记录(1024×4B 顺序追加)+ `index.json`(id→片号/偏移,约 40B/条) | ~100 万条 | 半天 + 迁移脚本 | 丢可读性(base64 本来也不可读) |
|
|
579
|
+
| **③ SQLite / 磁盘 ANN + 量化** | `vectors(id TEXT PK, vec BLOB)`;配 int8 量化 | 千万级 | 大,与第 10 节一起 | 原生依赖、迁移重 |
|
|
580
|
+
|
|
581
|
+
**推荐路径**:① 已完成(撑到十万条);过 5 万条做 ②;③ 跟 HNSW 一起上。
|
|
582
|
+
|
|
583
|
+
### ① 实施记录(2026-10-31)
|
|
584
|
+
|
|
585
|
+
改动全在 `createStore`(memory5.mjs),**调用方零改动**:
|
|
586
|
+
|
|
587
|
+
- 每片最多 **5000 条**(约 26MB),文件名 `vectors-part-0000.json` 起,序号定读取顺序;
|
|
588
|
+
- 写入:全量重写所有分片 + **清掉多余旧片**(条目变少时);`vectors` 为空时清空全部片;
|
|
589
|
+
- 读取:按序号依次并入,片间重复 id 只认第一份(防异常数据);
|
|
590
|
+
- **旧库自动迁移**:没有分片但存在旧 `vectors.json` 时,读出 → 写分片 → 旧文件改名 `vectors.json.bak`(保留回退,下次不再读);
|
|
591
|
+
- 快照工具:`mem.dumpVectors(path)` 合并导出单文件(给分析/调试用,库大时慎用)。
|
|
592
|
+
|
|
593
|
+
实测(专项脚本):12001 条 → 3 片(26.1/26.1/10.5MB),重载条数与首末值全对;
|
|
594
|
+
删到 4000 条后多余分片自动清理;旧格式迁移后 `.bak` 生成、原文件不再被读。
|
|
595
|
+
78 项回归全过(41 + 20 + 17)。
|
|
596
|
+
|
|
597
|
+
**顺带修的一处**:`simulate-big.mjs` / `simulate-m5.mjs` 原来用 `copyFileSync` 拷 `vectors.json` 做快照,
|
|
598
|
+
分片后文件不存在了,改成调 `mem.dumpVectors()`。
|
|
599
|
+
|
|
600
|
+
### ② 的关键设计(真做时照这个来)
|
|
601
|
+
|
|
602
|
+
- `vectors.bin` 里每条**定长 4096 字节**,顺序追加;偏移 = 序号 × 4096,不用额外存储;
|
|
603
|
+
- `index.json` 只存 `{ dim: 1024, ids: ["id1","id2",...] }`(数组顺序对应记录顺序),20 万条约 8MB,远低于 JSON 上限;
|
|
604
|
+
- 读取用 `fs.readSync` + `Float32Array` view **零拷贝**,不经过 JSON.parse;
|
|
605
|
+
- 删除用**墓碑 + 定期压缩**(与第 10 节 HNSW 的删除策略一致,正好同一套失效标记)。
|
|
606
|
+
|
|
607
|
+
### 终局问题:内存(比存储更晚爆,但更致命)
|
|
608
|
+
|
|
609
|
+
存储解决了,**内存还没解决**。检索是全扫余弦,所有向量必须在内存:
|
|
610
|
+
|
|
611
|
+
| 条数 | 内存(float32,4KB/条) | 结论 |
|
|
612
|
+
|---|---|---|
|
|
613
|
+
| 20 万 | 800MB | 勉强 |
|
|
614
|
+
| 50 万 | 2GB | 危险 |
|
|
615
|
+
| 100 万 | **4GB** | Node 必 OOM |
|
|
616
|
+
|
|
617
|
+
真终局是 **int8 量化**(4KB→1KB/条,召回损失 <1%)或 **mmap + 磁盘 ANN**。这跟存储是两件事,别混。
|
|
618
|
+
|
|
619
|
+
### 代码落点
|
|
620
|
+
|
|
621
|
+
| 位置 | 改什么 | 状态 |
|
|
622
|
+
|---|---|---|
|
|
623
|
+
| `createStore` 读 | 分片按序读;无分片时读旧单文件并迁移 | ✅ 已做 |
|
|
624
|
+
| `createStore.save()` / `saveVectors()` | 写分片 + 清多余旧片 | ✅ 已做 |
|
|
625
|
+
| `mem.dumpVectors(path)` | 合并导出快照(调试/分析) | ✅ 已做 |
|
|
626
|
+
| `simulate-big` / `simulate-m5` 快照 | `copyFileSync` → `mem.dumpVectors()` | ✅ 已做 |
|
|
627
|
+
| ② 二进制 + 偏移索引 | 见上方设计 | 未做 |
|
|
628
|
+
| ③ SQLite / 磁盘 ANN | 与第 10 节合并 | 未做 |
|
|
629
|
+
|
|
630
|
+
### 与第 10 节的关系
|
|
631
|
+
|
|
632
|
+
第 10 节「明确不做」里曾写「Float32Array 落盘只省 25%,等撞上 RangeError 再做」。
|
|
633
|
+
**现在这条升级为正式方案**——不是为省空间,是为绕开 JSON 字符串上限。做的时候两节合并实施。
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## 13. 文件结构
|
|
638
|
+
|
|
639
|
+
| 文件 | 作用 |
|
|
640
|
+
|---|---|
|
|
641
|
+
| `memory5.mjs` | 核心:六层、写入、路由、融合、检索、遗忘、存储分片 |
|
|
642
|
+
| `store-<writer>.json` | 条目元数据,**按写者分片**(2026-11-01 起)。单写者时就是一份;旧 `store.json` 首次自动认领 |
|
|
643
|
+
| `vectors-<writer>-part-*.json` | 向量分片,每片 ≤5000 条,**同样按写者分**(旧无前缀文件首次自动认领) |
|
|
644
|
+
| `config.json` | 运行配置。注意 **`dir` 不在这里**(它就在这个目录里,鸡生蛋),目录改在 `~/.dsh/memory-tiers.json` |
|
|
645
|
+
| `archive/{channel}/{id}.md` | 深层原文归档 |
|
|
646
|
+
| `graveyard/forgotten.jsonl` | 遗忘台账 |
|
|
647
|
+
| `silicon.mjs` | 模型调用:embed / classify / relation / summarize / rerank,含 token 预算闸 |
|
|
648
|
+
| `simulate-big.mjs` | 压测:按天推进 + 心跳 + 分段计时 + 健康检查 |
|
|
649
|
+
| `watch.mjs` | 看门狗:心跳超 3 分钟判卡死 |
|
|
650
|
+
| `build-dataset.mjs` | 造测试数据集 |
|
|
651
|
+
| `memory6.mjs` | v0.8.0 起的**默认**实现:六文件布局(L1 + temp + L2/L3/DL1~3),钥匙整理。见第 26 节 |
|
|
652
|
+
| `test-memory6.mjs` | 六文件布局的端到端测试(60 项) |
|
|
653
|
+
| `test-m5.mjs` / `test-batch.mjs` / `test-share.mjs` | legacy 回归(41 + 20 + 25 项,最后那个测多写者分片) |
|
|
654
|
+
|
|
655
|
+
### 多写者:一个目录给多个程序共用(2026-11-01,v0.6.0)
|
|
656
|
+
|
|
657
|
+
> 需求:dsh 和 astrbot **共用同一份记忆**。目录可以随便指,但两个程序同时写怎么办?
|
|
658
|
+
|
|
659
|
+
**先说原来的写法为什么不行**:`save()` 是 `writeFileSync(storePath, 整个 state)`——
|
|
660
|
+
整文件覆盖。两个进程同时跑:
|
|
661
|
+
|
|
662
|
+
1. **后写的整份覆盖先写的** → 对方这一轮记的东西直接没了
|
|
663
|
+
2. 写不是原子的 → 另一个进程可能读到半个 JSON
|
|
664
|
+
3. **ID 会撞**:`m5_<进程内计数器>_<时间>`,两个进程都从 1 开始
|
|
665
|
+
4. 分层迁移 / 遗忘 / 融合各自跑一遍 → 同一批条目被处理两次
|
|
666
|
+
|
|
667
|
+
### 解法:分片联邦(各写各的,读的时候合并)
|
|
668
|
+
|
|
669
|
+
```
|
|
670
|
+
memory/
|
|
671
|
+
store-dsh.json ← dsh 只写这份
|
|
672
|
+
store-astrbot.json ← astrbot 只写这份
|
|
673
|
+
config.json ← 共用一份配置(模型、容量…)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
- **写**:每个写者只写自己的 `store-<writer>.json`,临时文件 + `rename` 原子落盘
|
|
677
|
+
- **读**:启动时把所有 `store-*.json` 合并成一个视图;别人的条目标 `__foreign`
|
|
678
|
+
- **改**:分流、遗忘、压缩、融合**只动自己的**(`byLayer()` 默认只返回自己的)
|
|
679
|
+
- **看**:检索、`buildBlock`、`stats`、`list` 用 `{ all: true }` 看全库
|
|
680
|
+
|
|
681
|
+
### 融合的边界:跨文件不合并、不取代(2026-11-01 修)
|
|
682
|
+
|
|
683
|
+
分完片之后真正容易出事的是**融合**(新摘要入 DL3 时的判重)。它原来是遍历
|
|
684
|
+
`store.items` 找相似摘要,而 `store.items` 里含别人的条目:
|
|
685
|
+
|
|
686
|
+
```
|
|
687
|
+
判成 duplicate → mergeSummaries(对方的条目, 我的新摘要) + removeItem(我的新摘要)
|
|
688
|
+
↑ 合进去了,但落在别人的对象上,save() 只写自己的分片 → 存不回去
|
|
689
|
+
↑ 删除写进了自己的分片 → 永久生效
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
一合一删之后,这条内容**两边都不剩**:我的分片里没了,对方的分片里从来没有。
|
|
693
|
+
这是分片方案里唯一会**丢数据**的洞,比"重复留两条"严重得多。
|
|
694
|
+
|
|
695
|
+
现在的硬边界(四处,全是"别人的条目只读"):
|
|
696
|
+
|
|
697
|
+
| 位置 | 改法 |
|
|
698
|
+
|---|---|
|
|
699
|
+
| `moveTo()` | 遇到 `__foreign` 直接返回:不搬层(搬了存不回去,还会污染本次会话的注入) |
|
|
700
|
+
| `fuseIntoDL3()` | 候选池跳过 `__foreign`:只跟自己的摘要比重复 / 矛盾 / 进展 |
|
|
701
|
+
| `markSupersede()` | 连带作废引用摘要时跳过外来摘要(打不上标记) |
|
|
702
|
+
| `attend()` | 外来条目不加热度、不加关注次数(加了重启就没,两次查询排序不一致) |
|
|
703
|
+
|
|
704
|
+
自己库内的融合完全不受影响——上面四处只在 `it.__foreign` 时才生效。
|
|
705
|
+
|
|
706
|
+
注入侧还有一条配套的:容量是我们按自己的条目维护的(`enforceLimits` 只动自己的),
|
|
707
|
+
别人的条目直接叠上来会把块撑大。所以 `buildBlock` 里**自己的排前面、全量进**,
|
|
708
|
+
别人的用剩下的空间(自己那份已满时仍留 30% 给共享方,不然共享就没意义了)。
|
|
709
|
+
|
|
710
|
+
**不需要锁**——谁也不覆盖谁。这是选它而不是"文件锁 + 读改写合并"的原因:
|
|
711
|
+
锁方案还得处理"本进程删掉的条目被重读救回来"(要墓碑),复杂度高得多。
|
|
712
|
+
|
|
713
|
+
### 对方的条目什么时候能看见
|
|
714
|
+
|
|
715
|
+
用**文件指纹**(文件名 + mtime + size)判断,而不是定时刷新:
|
|
716
|
+
|
|
717
|
+
```js
|
|
718
|
+
function maybeRefreshForeign() {
|
|
719
|
+
const sig = foreignSignature() // 几次 stat,很便宜
|
|
720
|
+
if (sig === foreignSig) return false
|
|
721
|
+
refreshForeign() // 真变了才重读
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
调用点:检索前、注入前、统计前,以及每次自己 `save()` 前。
|
|
726
|
+
**对方一落盘,我这边下一次检索就能看到**——不是"最多延迟 15 秒"。
|
|
727
|
+
(一开始写的是 15 秒 TTL,实测发现会正好错过对方刚写的内容,改掉了。)
|
|
728
|
+
|
|
729
|
+
条目集合换过一代就 `gen++`,分层的索引缓存据此失效,否则会漏掉新合并进来的条目。
|
|
730
|
+
|
|
731
|
+
### 配置
|
|
732
|
+
|
|
733
|
+
```json
|
|
734
|
+
{ "writer": "dsh" } // 每个程序必须不同;同名 = 互相覆盖
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
数据目录本身三选一:插件配置 `dir` > **指针文件** `~/.dsh/memory-tiers.json` 里的
|
|
738
|
+
`{"dir": "..."}` > 环境变量 `MEMORY_TIERS_DIR` > 默认 `~/.dsh/storages/memory-v5`。
|
|
739
|
+
指针文件是为了让设置页能改目录——`config.json` 在目录里面,没法自己声明目录在哪。
|
|
740
|
+
|
|
741
|
+
### 已知限制(接受,不打算现在解决)
|
|
742
|
+
|
|
743
|
+
| 限制 | 原因 | 后果 |
|
|
744
|
+
|---|---|---|
|
|
745
|
+
| **不跨写者去重/取代** | 取代要改对方的条目(`supersededBy`),而对方的分片我们没资格写 | 两边记了同一件事会各留一条 |
|
|
746
|
+
| **不跨写者融合** | 同上,融合要合并两条、删掉一条(详见上节:跨写者融合会丢数据,已显式禁掉) | DL3 里可能有语义重复的摘要 |
|
|
747
|
+
| 检索命中别人条目时热度加不上去 | 热度的 `attend()` 要改条目 | 对方的热度只由他自己更新 |
|
|
748
|
+
| 别人的条目没有向量 | 向量也按写者分片(`vectors-<writer>-part-*`),我们只加载自己的 | 搜别人的记忆靠关键词,语义分用不上 |
|
|
749
|
+
| L3 视图最多延迟一个刷新周期 | 它本来就有 `viewRefreshMs` 节流 | 新沉淀的摘要晚一会儿进上下文 |
|
|
750
|
+
|
|
751
|
+
真要跨写者去重,得引入"覆盖表"(各自记 `override: {id: supersededBy}`,读时应用)——
|
|
752
|
+
现在没这个需求,先不做。
|
|
753
|
+
|
|
754
|
+
### 实测
|
|
755
|
+
|
|
756
|
+
```
|
|
757
|
+
两个写者交替各写 5 条 → store-dsh.json 5 条 / store-astrbot.json 5 条(互不覆盖)
|
|
758
|
+
dsh 重新打开 → 看到 10 条(自己的 5 + 对方的 5)
|
|
759
|
+
dsh 再落盘 → 自己的分片还是 5 条(没把别人的收进来)
|
|
760
|
+
对方新增一条 → 自己下次检索就看到(11 条)
|
|
761
|
+
旧 store.json → 只被第一个写者认领一次,第二个写者不会重复认领
|
|
762
|
+
|
|
763
|
+
dsh 侧检索 "表情包" → 命中 astrbot 写的那条
|
|
764
|
+
注入块 → 同时含两边的 L1
|
|
765
|
+
让 dsh 的 L1 溢出 → 只分流自己的(对方的 L1 原封不动)
|
|
766
|
+
|
|
767
|
+
融合边界(第 5 组,7 项):两条内容相同、向量相同的摘要,一个属于 dsh 一个属于 astrbot
|
|
768
|
+
dsh 的新摘要入 DL3 → 不与对方的合并(返回 null),自己这条还在
|
|
769
|
+
对方的条目 text / layer / heat / supersededBy / fusionCount 全都没变
|
|
770
|
+
dsh 自己库内的第二条重复摘要 → 照样合并(删掉第二条)
|
|
771
|
+
各自落盘 → 分片里仍然只有自己写的
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## 14. 模型提示词清单(改之前先看这节)
|
|
777
|
+
|
|
778
|
+
> 提示词是**行为的一部分,但回归测不出来**(测试用假模型)。改一句可能让 78 项全过、实际效果全变。
|
|
779
|
+
|
|
780
|
+
### 各环节提示词
|
|
781
|
+
|
|
782
|
+
| 环节 | 函数 | 位置 | 要点 |
|
|
783
|
+
|---|---|---|---|
|
|
784
|
+
| 分类 | `classify` / `classifyBatch` | `silicon.mjs` `CLASSIFY_SYS` + 批量版 | 三档定义 + 10 个示例;「0.85 一天不超过一两条」「做了事拿到结果是 0.6 不是 0.85」;本地规则前置(`LOW/HIGH_PATTERNS`)省钱 |
|
|
785
|
+
| 摘要(主路径) | `summarizeWithSources` | `silicon.mjs` ~580 行 | 摘要 + 出处编号 + ★/·/+ 三档;**条数上限 n/10 是刹车不是任务量**;一条只讲一件事;人名必留;`+` 约二三成 |
|
|
786
|
+
| 摘要(无出处) | `summarize` | `silicon.mjs` ~470 行 | 按 channel 换重点(action/dialog/event 三套 focus);1-3 行,`·` 开头,每条 ≤40 字 |
|
|
787
|
+
| 判关系 | `relation` | `silicon.mjs` ~836 行 | 四类:duplicate / contradiction / update / unrelated;**update 不能误判成 contradiction**(否则删掉进展史) |
|
|
788
|
+
| 判同一件事(旧) | `sameEvent` | `silicon.mjs` ~814 行 | 极简 yes/no,已不是主路径 |
|
|
789
|
+
| 向量 / 精排 / 压缩 | — | — | 向量和 rerank 不需要提示词(API 自带打分);**compress 有意停用**(实测模型压缩一个字都减不掉) |
|
|
790
|
+
|
|
791
|
+
### 三个跨环节的硬约束(改提示词时别弄丢)
|
|
792
|
+
|
|
793
|
+
1. **必须 `thinking: false`**。不传会开推理,输出被 maxTokens 截断——历史事故:输出 295K token 是输入的 2.4 倍,钱全烧在被截断的思考上。
|
|
794
|
+
2. **单块上限 `summaryChunk: 60`**。分块会重复付 system 提示词的钱;大模型按输入线性计费,一次 60 条最省。
|
|
795
|
+
3. **人名和数字必须保留**。摘要把「张三说要爬山」压成「聊了爬山」,这条记忆就废了。
|
|
796
|
+
|
|
797
|
+
### 改提示词的验证流程(必须是这套,别偷懒)
|
|
798
|
+
|
|
799
|
+
```
|
|
800
|
+
① 改 silicon.mjs 的提示词
|
|
801
|
+
② 真实模型跑 20~50 条样本(不能用 test-m5 的假模型)
|
|
802
|
+
③ 人工对比:摘要是否丢人名/数字、是否硬凑、★ 比例是否合理
|
|
803
|
+
④ 只有 ①②③ 都过,才跑 78 项回归确认没把代码改坏
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
回归只能证明「代码没坏」,证明不了「提示词变好」。两者缺一不可,顺序不能反。
|
|
807
|
+
|
|
808
|
+
---
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## 25. 工作模式(**未实施**,2026-11-01 备忘)
|
|
812
|
+
|
|
813
|
+
> 用户提的问题:**工作模式必然需要大量上下文**,而 L1 容量 + 写入捕获的限制
|
|
814
|
+
> 让它记不全。先记思路,没动手。
|
|
815
|
+
|
|
816
|
+
### 矛盾在哪
|
|
817
|
+
|
|
818
|
+
现在的默认设计是「dsh 历史一条不留,全交给记忆」(`compactKeepTurns: 0`)。
|
|
819
|
+
日常聊天没问题,但**工作模式**(写代码、查日志、长任务)会撞到三个限制:
|
|
820
|
+
|
|
821
|
+
| 限制 | 默认值 | 工作模式下的后果 |
|
|
822
|
+
|---|---|---|
|
|
823
|
+
| `memoryCfg.hotMaxChars`(L1) | 2000 字 | 记不下几个来回就满 |
|
|
824
|
+
| `toolCaptureChars` | 500 字 | `cat` 一个文件、`npm run build` 的输出被砍成 500 字 |
|
|
825
|
+
| `userCaptureChars` | 2000 字 | 长指令被截断 |
|
|
826
|
+
|
|
827
|
+
于是模型在工作模式里**看不到刚才读了什么文件、命令输出了什么**——
|
|
828
|
+
而这些恰恰是它继续干活必须的。
|
|
829
|
+
|
|
830
|
+
### 关键洞察:工作记忆不该走 L1
|
|
831
|
+
|
|
832
|
+
看着像「把 L1 撑大就行」,但算一下成本会发现反直觉:
|
|
833
|
+
|
|
834
|
+
| 方案 | 信息完整度 | 缓存 |
|
|
835
|
+
|---|---|---|
|
|
836
|
+
| **撑大 L1**(hotMaxChars → 2 万字) | 仍被 `toolCaptureChars` 截断,且是**重写过的**转述 | 快照是**替换语义**(新快照 shadow 旧的),每轮全新 → **全价** |
|
|
837
|
+
| **保留几轮 dsh 历史**(keepTurns → 6~10) | **原文原样**,工具输出完整、没有截断 | 历史是 **append 语义**,前面轮次稳定 → **缓存友好** |
|
|
838
|
+
|
|
839
|
+
**同样信息量下,「少压一点历史」可能比「把 L1 撑大」更便宜**,而且信息还更完整。
|
|
840
|
+
L1 的定位是「沉淀用的原料」,不是「当前任务的草稿纸」。
|
|
841
|
+
|
|
842
|
+
### 三个可选方案(按代价递增)
|
|
843
|
+
|
|
844
|
+
| 方案 | 做法 | 评价 |
|
|
845
|
+
|---|---|---|
|
|
846
|
+
| **A · 纯调参** | `hotMaxChars` 调到 1~2 万、`toolCaptureChars` 调到 3000~5000 | 立刻能用,但每轮全价付 1~2 万字,且不解决截断 |
|
|
847
|
+
| **B · 工作模式开关**(推荐) | 一个 `workMode` 开关(或 `/memory work` 命令):开启时 `compactKeepTurns` 临时放宽到 6~10、工具捕获放宽;关闭时回到 0 | 直击要害:工作记忆交给 dsh 历史,L1 照旧只管沉淀。要改压缩调度 |
|
|
848
|
+
| **C · 独立工作区** | 新增短暂层:当前任务的文件片段 / 命令输出按任务聚合,任务结束把结论沉进 L3 后清空 | 最彻底,但要新增一层 + 生命周期管理,工作量大 |
|
|
849
|
+
|
|
850
|
+
### 如果要做 B,几个待定的点
|
|
851
|
+
|
|
852
|
+
- 怎么触发:配置项(要重启)还是命令(运行时切)?倾向命令——工作模式是临时的
|
|
853
|
+
- 退出时:保留的历史要按现在的规则压掉并归档,不能留着涨
|
|
854
|
+
- 与 `refreshMinMs` 无关:L3 排序该节流还是节流
|
|
855
|
+
|
|
856
|
+
> **分区 C · dsh 接入**(15–19 节)— 上下文接入、历史压缩、成本、适配层速查
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## 15. 接入方式:上下文 ↔ 记忆
|
|
861
|
+
|
|
862
|
+
> **状态:已接入(2026-10-31)。** `index.mjs` 已从 v1(`memory.mjs` 三层)切到 **v5(`memory5.mjs` 六层)**。
|
|
863
|
+
> 数据目录换成 `~/.dsh/storages/memory-v5`(v1 的旧库不迁移,留在原目录)。
|
|
864
|
+
> 插件是 link 安装,改完**重启 dsh 生效**。
|
|
865
|
+
|
|
866
|
+
### 装在哪(踩过的坑)
|
|
867
|
+
|
|
868
|
+
插件必须装进**你实际启动的那个 profile**:
|
|
869
|
+
|
|
870
|
+
| profile | 谁在用 | 装法 |
|
|
871
|
+
|---|---|---|
|
|
872
|
+
| `desktop` | 桌面版 DeepSeek Harness(Electron) | 手动改 `~/.dsh/profiles/desktop/package.json` 的 `dependencies` + `dsh.profile.bundles`,然后 `pnpm install` |
|
|
873
|
+
| `web` | `dsh web` 命令行起的实例 | `dsh plugin --profile web add ./` |
|
|
874
|
+
|
|
875
|
+
**症状**:装在 web、却启动桌面版 → 插件完全不加载、`storages/memory-v5` 不生成、没有任何报错。
|
|
876
|
+
判断方法:重启后看 `~/.dsh/storages/memory-v5/status.json` 是否存在。
|
|
877
|
+
|
|
878
|
+
### 配置方式:设置页(推荐)或 config.json
|
|
879
|
+
|
|
880
|
+
**v0.4.0 起有图形界面**:dsh 设置 → 记忆(见第 20 节)。
|
|
881
|
+
它是手搓的 React 组件,绕开了 `schemastery`——那个方案走不通,
|
|
882
|
+
因为 link 安装的插件 **import 不到 dsh 自己的包**(和 `defineTool` 同一个坑)。
|
|
883
|
+
|
|
884
|
+
> ⚠️ 设置页**改完要重启 dsh** 才生效(保存只写 JSON,不重建运行中的实例)。
|
|
885
|
+
> 而且它只暴露常用字段,模型那部分在 v0.6.0 又改版过(见第 24 节)。
|
|
886
|
+
|
|
887
|
+
所以直接改 JSON 依然是有效途径,两边写的是同一个文件:
|
|
888
|
+
|
|
889
|
+
```
|
|
890
|
+
~/.dsh/storages/memory-v5/config.json
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
首次启动自动生成示例。字段:
|
|
894
|
+
|
|
895
|
+
| 字段 | 默认 | 说明 |
|
|
896
|
+
|---|---|---|
|
|
897
|
+
| `apiKey` | `''` | 硅基流动 key;留空则语义检索关闭(只走关键词,无假向量) |
|
|
898
|
+
| `apiBase` | `https://api.siliconflow.cn/v1` | 也可用环境变量 `SILICONFLOW_API_KEY` |
|
|
899
|
+
| `preset` | `'siliconflow'` | 嵌入/重排用哪套:`siliconflow` = 免费 bge 组合,`custom` = 用 `models` 里填的(第 24 节) |
|
|
900
|
+
| `models` | chat/summary 走 `source:'session'` | 每个阶段独立指定来源,见第 24 节 |
|
|
901
|
+
| `noThink` | `{chat:true, summary:false}` | 是否关思考,见第 24 节 |
|
|
902
|
+
| `bootMaxChars` | 4200 | 每轮注入上下文的最大字数 |
|
|
903
|
+
| `autoCapture` | true | 是否自动捕获对话 |
|
|
904
|
+
| `userCaptureChars` | 2000 | 单条用户消息截断 |
|
|
905
|
+
| `toolCaptureChars` | 500 | 工具输出截断 |
|
|
906
|
+
| `budgetTokens` | 0 | 进程级 token 预算,0 不限 |
|
|
907
|
+
|
|
908
|
+
### 怎么用
|
|
909
|
+
|
|
910
|
+
| 场景 | 做什么 |
|
|
911
|
+
|---|---|
|
|
912
|
+
| 自动记忆 | **什么都不用做**。每轮 `turn/end` 自动把用户消息和工具行为写进 L1,`deferred` 摘要后台跑 |
|
|
913
|
+
| 查看状态 | `/memory`(概览);`/memory list`;`/memory search 关键词`;`/memory flush`;`/memory sweep` |
|
|
914
|
+
| 模型主动记 | `memory_write` 工具(补充自动捕获漏掉的结论) |
|
|
915
|
+
| 模型主动查 | `memory_search` → 确认相关后 `memory_attend`(搜到不涨热度) |
|
|
916
|
+
| 排查 | 看 `status.json` 的 `promptInjected` / `semantic` 字段;日志里 `[memory-tiers]` 前缀 |
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
### 两条路径
|
|
920
|
+
|
|
921
|
+
```
|
|
922
|
+
写入(上下文 → L1):
|
|
923
|
+
每条消息 → mem.write({ text, speaker, people, scene, channel, at })
|
|
924
|
+
→ commitOne:embed + 进 L1(不判重、不分类、不摘要)
|
|
925
|
+
→ 溢出时按 deferred 入队 → 空闲 flushSummaries() 沉淀
|
|
926
|
+
|
|
927
|
+
读取(记忆 → 上下文):
|
|
928
|
+
每轮组装 prompt → mem.buildBlock(budget, { includeHot: false })
|
|
929
|
+
├─ 「一直记得的」= L3 视图(DL3 按 importance×heat 截前 N)
|
|
930
|
+
└─ 「最近沉淀的」= L2
|
|
931
|
+
(「刚发生的」= L1 默认不注入,理由见下节)
|
|
932
|
+
→ ctx.systemPrompt.context({ name:'memory-tiers', order:250, text })
|
|
933
|
+
★ 是 context(追加式 user 快照),不是 section(塞系统提示词)
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
### 写入 L1 的三种方案(关键决策)
|
|
937
|
+
|
|
938
|
+
| 方案 | 谁触发 | 优点 | 缺点 |
|
|
939
|
+
|---|---|---|---|
|
|
940
|
+
| **A 模型主动**(v1 现状) | 模型调 `memory_write` 工具 | 简单;模型判断质量高 | **漏记、记得太碎、会自己去重**——v5 的核心信号「重复度 = 重要性」直接失效 |
|
|
941
|
+
| **B 自动捕获**(v5 设计意图)✅ | 订阅 `session/event`,按 turn 写入 | 全量、不靠自觉;重复度信号完整 | 需要过滤系统注入;工具输出要截断 |
|
|
942
|
+
| **C 提示词要求** | 在 systemPrompt 里让模型每轮自己写 | 零改造 | 不稳定,等于 A 的弱化版 |
|
|
943
|
+
|
|
944
|
+
**结论:走 B,钩子已经确认存在。** dsh 的 session 是事件溯源(event-sourced)的,
|
|
945
|
+
每次 append 都会 post-commit 广播:
|
|
946
|
+
|
|
947
|
+
```js
|
|
948
|
+
ctx.on('session/event', (session, event) => {
|
|
949
|
+
// event.type ∈ user/message | assistant/message | tool/call | tool/result | turn/end | ...
|
|
950
|
+
// event.data 是对应 payload
|
|
951
|
+
})
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**可用的事件(dsh-session 的 `KNOWN_SESSION_EVENT_TYPES` 里挑出来的)**:
|
|
955
|
+
|
|
956
|
+
| 事件 | payload | 记忆里对应什么 |
|
|
957
|
+
|---|---|---|
|
|
958
|
+
| `user/message` | `UserMessage`(`content: ContentBlock[]`、`source`) | 用户原话 → `channel='dialog'` |
|
|
959
|
+
| `assistant/message` | `AssistantMessage`(含 `usage`) | AI 回复 → `channel='dialog'`(可选,可截断) |
|
|
960
|
+
| `tool/call` | `name`、`arguments`(原始 JSON 字符串)、`callId` | 行为的前半:调了什么 |
|
|
961
|
+
| `tool/result` | `message: ToolResultMessage`、`isError`、`callId` | 行为的后半:结果如何 |
|
|
962
|
+
| `turn/start` / `turn/end` | `{ turn }` / `{ turn, reason }` | **收口时机**:一轮结束再写 |
|
|
963
|
+
|
|
964
|
+
### 「事件」和「行为」怎么进 L1
|
|
965
|
+
|
|
966
|
+
核心原则:**按 turn 聚合,不逐条事件写**。一轮对话结束时(`turn/end`)把这一轮合成 1~N 条记忆:
|
|
967
|
+
|
|
968
|
+
```
|
|
969
|
+
一轮对话:
|
|
970
|
+
user/message 「帮我把测试跑一下」 ┐
|
|
971
|
+
assistant/message 「好,我跑一下」 │
|
|
972
|
+
tool/call bash: npm test ├─ 合成:
|
|
973
|
+
tool/result 「3 个失败:test-m5 …」 │
|
|
974
|
+
assistant/message 「3 个失败,我看了下…」 ┘
|
|
975
|
+
|
|
976
|
+
写入:
|
|
977
|
+
{ text: '用户:帮我把测试跑一下', channel: 'dialog', speaker: 'user' }
|
|
978
|
+
{ text: '运行 bash「npm test」→ 3 个失败:test-m5…', channel: 'action', speaker: 'bot' }
|
|
979
|
+
{ text: 'AI:3 个失败,我看了下…', channel: 'dialog', speaker: 'bot' }
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
**六个必须处理的细节**:
|
|
983
|
+
|
|
984
|
+
1. **过滤系统注入**。`user/message` 的 `source` 不只有人类——`agent.inject()`、skill 内容、
|
|
985
|
+
cron 通知、子目录 AGENTS.md 都走这个事件。只记 `source` 是人类输入的那类,否则 L1 会被
|
|
986
|
+
系统注入污染,而且每轮重复注入会让「重复度=重要性」信号失真。
|
|
987
|
+
2. **只取 text 块**。`content` 是 `ContentBlock[]`,还有 `reasoning`(思考)和 `image`。
|
|
988
|
+
思考过程不该进记忆——它是模型的草稿,不是事实。
|
|
989
|
+
3. **工具调用要配对**。`tool/call` 只有 name+arguments,`tool/result` 才有结果,靠 `callId` 配对;
|
|
990
|
+
一轮可能有多个 step(多次工具调用),在 `turn/end` 统一收口。
|
|
991
|
+
4. **工具输出截断**。bash 输出动辄几万字,记前 500 字 + `…(截断)`,否则 L1 一次就被撑爆。
|
|
992
|
+
5. **`turn/end` 才写**。中途写会得到半截对话(问了还没答),而且写入次数多一倍。
|
|
993
|
+
6. **写完即 flush(可选)**。`deferred` 模式下 turn/end 写入不阻塞,空闲调 `flushSummaries()`。
|
|
994
|
+
|
|
995
|
+
### v5 接入改造清单(已完成)
|
|
996
|
+
|
|
997
|
+
| # | 改什么 | 状态 |
|
|
998
|
+
|---|---|---|
|
|
999
|
+
| 1 | `package.json` 的 `files` 加 `memory5.mjs` / `silicon.mjs` | ✅ 已做(版本 0.2.0) |
|
|
1000
|
+
| 2 | `index.mjs` 换 `createMemory5` | ✅ 已做 |
|
|
1001
|
+
| 3 | 工具:`memory_write`(写一条) | ✅ 已做 |
|
|
1002
|
+
| 4 | 工具:`memory_search` + `memory_attend` | ✅ 已做(搜到不涨热度,确认才涨) |
|
|
1003
|
+
| 5 | 注入:`systemPrompt.section` | ✅ 已做(`buildBlock`,`order: 2600`) |
|
|
1004
|
+
| 6 | 订阅 `session/event` | ✅ 已做(按 turn 聚合 + 过滤系统注入 + 工具输出截断) |
|
|
1005
|
+
| 7 | 定时 flush / sweep | ✅ 已做(5 分钟 flush,6 小时 sweep) |
|
|
1006
|
+
| 8 | 无 key 降级 | ✅ 已做(v0.5.2 起直接关闭语义层,只用关键词,不造假向量) |
|
|
1007
|
+
|
|
1008
|
+
**冒烟验证**(假 ctx):5 个工具注册、`session/event` 订阅、一轮对话写入 2 条
|
|
1009
|
+
(用户消息 + 工具行为,skill 注入被正确过滤)、`buildBlock` 注入正常、78 项回归全过。
|
|
1010
|
+
|
|
1011
|
+
### 自动捕获的实现要点
|
|
1012
|
+
|
|
1013
|
+
- **按 turn 聚合**:`user/message` → 缓冲;`tool/call` + `tool/result` 按 `callId` 配对;
|
|
1014
|
+
`turn/end` 时一次性 `writeMany`。
|
|
1015
|
+
- **过滤**:只记 `source.kind === 'user'` 的输入(`agent.inject` / skill / cron 的 kind 不是它)。
|
|
1016
|
+
- **截断**:用户消息 ≤2000 字,工具输出 ≤500 字(bash 一次能吐几万字,L1 才 1500 字)。
|
|
1017
|
+
- **失败兜底**:没有 embedding key 时关闭语义层、只走关键词(v0.5.2 起不再用哈希向量充数);写入失败只打日志,不影响对话。
|
|
1018
|
+
|
|
1019
|
+
### 启动时要做的一件事
|
|
1020
|
+
|
|
1021
|
+
v1 是 `mem.decay()`(补结算关机以来的遗忘);v5 对应的是 `mem.sweep()`——它会执行完整的分层收敛(作废条沉底、L1/L2 溢出、DL1 遗忘)。**启动不扫一次,停机期间该忘的不会忘。**
|
|
1022
|
+
|
|
1023
|
+
---
|
|
1024
|
+
|
|
1025
|
+
## 16. 上下文成本:为什么不能塞系统提示词(2026-10-31 修)
|
|
1026
|
+
|
|
1027
|
+
> 症状:每轮只发一句话,账单上却涨好几万 token。实测定位后修掉,本节记过程和结论。
|
|
1028
|
+
|
|
1029
|
+
### 实测数据(dsh 桌面版,deepseek-flash,4 轮简单对话)
|
|
1030
|
+
|
|
1031
|
+
会话记录 `~/.dsh/sessions/*/session.v4.jsonl.zstd` 里每次 `assistant/message` 都带 `usage`:
|
|
1032
|
+
|
|
1033
|
+
| 轮次 | inputTokens(未命中) | cacheReadTokens | 说明 |
|
|
1034
|
+
|---|---|---|---|
|
|
1035
|
+
| 1 | 12715 | 0 | 首次,全量计费,正常 |
|
|
1036
|
+
| 2 | **4018** | 12800 | 本该只有几百(新增消息) |
|
|
1037
|
+
| 3 | 4034 | 16896 | 同上 |
|
|
1038
|
+
| 4 | 4009 | 25600 | 同上 |
|
|
1039
|
+
|
|
1040
|
+
`totalTokens` 是累加值(12847 → 16926 → 21310 → 25609 → 29815),**界面上看到的「好几万」就是这个累计数**。真正的问题是每轮有约 4000 token 未命中缓存、按全价计费。
|
|
1041
|
+
|
|
1042
|
+
### 根因:记忆块坐在系统提示词正中间
|
|
1043
|
+
|
|
1044
|
+
dsh 的缓存是**前缀缓存**——从第一个 token 开始匹配,一旦某处变了,**它后面的全部失效**。
|
|
1045
|
+
|
|
1046
|
+
对齐两轮 system message 后发现:9729 字里前 6625 字(68%)一致,**分歧点正好是记忆块**:
|
|
1047
|
+
|
|
1048
|
+
```
|
|
1049
|
+
…(前 68% 稳定的身份/策略/工具说明)
|
|
1050
|
+
## 刚发生的 ← 记忆块从这里开始
|
|
1051
|
+
- 10-31 [user] 小鱼是你吗 ← 每轮都是新的
|
|
1052
|
+
(后面还有 3100 字稳定的工具说明,全被连累)
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
三个错叠在一起:
|
|
1056
|
+
|
|
1057
|
+
1. **位置错**:`order: 2600` 正好撞上 `SECTION_ORDERS.TOOL_WORKFLOW`,记忆块插在工具说明中间,
|
|
1058
|
+
把它后面的 3100 字稳定内容一起拖下水。
|
|
1059
|
+
2. **内容错**:L1「刚发生的」是当前会话的对话——**历史里本来就有**,注入等于每轮重说一遍,还每轮变。
|
|
1060
|
+
3. **机制错**:用 `section()` 塞系统提示词,变化必然导致后续前缀失效。
|
|
1061
|
+
|
|
1062
|
+
### 修法:换成追加式快照(含两处后续更正)
|
|
1063
|
+
|
|
1064
|
+
**① 用 `systemPrompt.context()` 代替 `section()`**
|
|
1065
|
+
|
|
1066
|
+
dsh 有两套注入接口,语义完全不同:
|
|
1067
|
+
|
|
1068
|
+
| | `section()` | `context()` |
|
|
1069
|
+
|---|---|---|
|
|
1070
|
+
| 去哪 | 拼进**系统提示词** | 作为 **user 角色快照**追加进历史 |
|
|
1071
|
+
| 变化后果 | 它之后的系统提示词全部重新计费 | 只多一条消息,**前面的历史缓存不受影响** |
|
|
1072
|
+
| 去重 | 无 | 框架比对文本,**内容没变就不追加**(`project()` 里的 `retained?.text === snapshot` 判断)|
|
|
1073
|
+
|
|
1074
|
+
系统提示词因此变成逐字节不变 → 首轮之后全量命中缓存。
|
|
1075
|
+
|
|
1076
|
+
**② 三层恒定注入(2026-11-01 定稿:`injectHot` 已删除)**
|
|
1077
|
+
|
|
1078
|
+
> **⚠️ 这条理由被推翻过一次**。原先写的是「L1 每轮都变 → 注入块每轮变化 → **打掉缓存**」,
|
|
1079
|
+
> 据此把 `injectHot` 默认关掉。但那条结论只在 `section()` 时代成立——记忆块塞在系统提示词里,
|
|
1080
|
+
> 一变后面全废。改用 `context()` 之后,记忆块是**追加**的 user 快照、位置在末尾,
|
|
1081
|
+
> **前面历史缓存完全不受影响**。用户当场指出了这点,是对的。
|
|
1082
|
+
>
|
|
1083
|
+
> 开关赖以存在的理由没了,所以不是「改个默认值」,而是**把 `injectHot` 整个删除**
|
|
1084
|
+
> (配置项和代码一起,现在根本不读它)。
|
|
1085
|
+
|
|
1086
|
+
现在的行为:**L1 / L2 / L3 恒定全部注入**,可调的只有各自容量
|
|
1087
|
+
(`memoryCfg.hotMaxChars` / `consolidateMaxChars` / `longMaxChars`)。
|
|
1088
|
+
|
|
1089
|
+
成本是每轮多追加一条快照(L1 通常几百到一千多字,按全价计费),换来的是
|
|
1090
|
+
**dsh 历史可以一条不留**——最近原话由 L1 提供,`compactKeepTurns` 因此能设成 0(见第 18 节)。
|
|
1091
|
+
|
|
1092
|
+
跨会话恢复始终靠 L2/L3(沉淀过的)+ `memory_search`(按需检索)。
|
|
1093
|
+
|
|
1094
|
+
**③ 节流(已推翻,现行机制见本节末尾)**
|
|
1095
|
+
|
|
1096
|
+
> **⚠️ 2026-11-01 重写**:这里原来的做法是「把**整个记忆块**压着 5 分钟不刷新」。
|
|
1097
|
+
> 这是错的——刚写进 L1 的话要等半天才进上下文,而快照是追加在末尾的,压住它没省下任何缓存。
|
|
1098
|
+
> 现行机制是**只节流 L3 视图(DL3 排序)**,见「记忆刷新:只有 L3 需要节流」。
|
|
1099
|
+
> 下面「效果」里的数字是 2026-10-31 实测,当时还是 `keepTurns=1` + 不注入 L1 的配置。
|
|
1100
|
+
|
|
1101
|
+
### 效果
|
|
1102
|
+
|
|
1103
|
+
- 注入块从 4055 字降到 3952 字(L1 只占 103 字,省的不多)
|
|
1104
|
+
- **但内容从「每轮必变」变成「几乎不变」**(正式库连续 3 次调用逐字节一致)
|
|
1105
|
+
- 系统提示词恢复逐字节稳定,每轮省约 4000 token 全价计费
|
|
1106
|
+
|
|
1107
|
+
### 这条经验推广到任何注入
|
|
1108
|
+
|
|
1109
|
+
> **凡是每轮可能变的内容,都不能放进系统提示词;要么走 `context()`,要么放最后。**
|
|
1110
|
+
|
|
1111
|
+
判断顺序:
|
|
1112
|
+
1. 内容会变吗?不变 → `section()` 随便放
|
|
1113
|
+
2. 会变 → 用 `context()`(追加,不破坏前缀)
|
|
1114
|
+
3. `context()` 也用不了 → `section()` 的 order 必须**大于** `SECTION_ORDERS.DEPLOYMENT_PERSONA_SUFFIX`(10200),保证后面没内容可污染
|
|
1115
|
+
|
|
1116
|
+
### 怎么自己复现诊断
|
|
1117
|
+
|
|
1118
|
+
```bash
|
|
1119
|
+
# 解开会话记录,看每轮 usage
|
|
1120
|
+
zstd -dc ~/.dsh/sessions/*/*/session.v4.jsonl.zstd | node -e "
|
|
1121
|
+
let b='';process.stdin.on('data',d=>b+=d).on('end',()=>{
|
|
1122
|
+
for(const l of b.split('\n')){const o=l&&JSON.parse(l);
|
|
1123
|
+
if(o&&o.type==='assistant/message'&&o.data?.usage)console.log(o.data.turn,JSON.stringify(o.data.usage))}
|
|
1124
|
+
})"
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
判读:**`inputTokens` 应该只有「新增消息」的量(几百),`cacheReadTokens` 才是历史量**。
|
|
1128
|
+
如果每轮 `inputTokens` 都是几千、且接近系统提示词的字数,就是系统提示词在被破坏。
|
|
1129
|
+
|
|
1130
|
+
### 后续:修复生效了,但那 6 万不是记忆的锅(2026-10-31 19:40)
|
|
1131
|
+
|
|
1132
|
+
重启后同一个会话的实测:
|
|
1133
|
+
|
|
1134
|
+
| 轮次 | 未命中 input | 缓存重读 cacheRead | 总输入 |
|
|
1135
|
+
|---|---|---|---|
|
|
1136
|
+
| 4.1(修前) | 4009 | 25600 | 29609 |
|
|
1137
|
+
| 5.1(切换那轮) | 4343 | 29696 | 34039 |
|
|
1138
|
+
| 5.2 | 176 | 34560 | 34736 |
|
|
1139
|
+
| 6.1 | 3011 | 56576 | 59587 |
|
|
1140
|
+
| 7.1 | **221** | 59648 | 59869 |
|
|
1141
|
+
| 8.1 | **156** | 59904 | 60060 |
|
|
1142
|
+
|
|
1143
|
+
系统提示词从 9885 字降到 **5775 字**(记忆块 3952 字移出),5.1 之后每轮未命中稳定在 **156~221 token**。修复有效。
|
|
1144
|
+
|
|
1145
|
+
**但总输入仍是 6 万。** 拆开看这 6 万是怎么来的(统计整个会话的字符构成):
|
|
1146
|
+
|
|
1147
|
+
| 成分 | 字符数 | 占比 |
|
|
1148
|
+
|---|---|---|
|
|
1149
|
+
| **工具结果**(Bash 输出、文件读取) | 55978 | **46.5%** |
|
|
1150
|
+
| system 提示词(只算最后一条 5775) | — | — |
|
|
1151
|
+
| assistant 思考(reasoning) | 5727 | 4.8% |
|
|
1152
|
+
| assistant 工具调用参数 | 2977 | 2.5% |
|
|
1153
|
+
| **记忆快照**(1 条,替换语义只保留最新) | 3952 | **约 4%** |
|
|
1154
|
+
| assistant 正文 | 873 | 0.7% |
|
|
1155
|
+
|
|
1156
|
+
**结论:上下文里记忆只占 4%,46.5% 是工具输出。** 一个干了大量文件读取/命令执行的会话,6 万上下文是正常水位,跟记忆插件无关。
|
|
1157
|
+
|
|
1158
|
+
两个容易误读的点:
|
|
1159
|
+
|
|
1160
|
+
1. **`totalTokens` 是会话累计值**,不是单轮消耗。界面上看到「6 万」往往是这个数累计到 6 万,或者 `input + cacheRead` = 上下文总长度。
|
|
1161
|
+
2. **`cacheReadTokens` 要计费**,但按缓存命中价(DeepSeek 是未命中的约 1/10)。6 万缓存 token 的钱 ≈ 6 千全价 token。
|
|
1162
|
+
|
|
1163
|
+
### 快照会累积吗?不会
|
|
1164
|
+
|
|
1165
|
+
`dsh-session` 里 `isReplacementSurfaceEvent()` 判定 `surfaceOp !== 'append'` 为替换:
|
|
1166
|
+
新快照会 **shadow 掉旧快照**,旧的不再进上下文(只在 session 日志里留痕)。所以快照再多也不会线性膨胀,
|
|
1167
|
+
但**每次刷新仍会带来一条新的几百上千 token 内容**(旧的那条才被释放)。
|
|
1168
|
+
|
|
1169
|
+
因此快照刷新本身**不需要节流**(内容没变就不追加)。真正需要节流的是 L3 的排序,见下节。
|
|
1170
|
+
|
|
1171
|
+
### 记忆刷新:只有 L3 需要节流(2026-11-01 改)
|
|
1172
|
+
|
|
1173
|
+
> 用户指出原设计是错的:**整个记忆块被压着 30 分钟不刷新**,
|
|
1174
|
+
> 结果刚写进 L1 的话要等半小时才进上下文。而快照是追加在末尾的,
|
|
1175
|
+
> 前面历史根本不受影响——没什么可省的。
|
|
1176
|
+
|
|
1177
|
+
**三层的位置决定谁该被节流**。注入块从上到下是旧到新:
|
|
1178
|
+
|
|
1179
|
+
```
|
|
1180
|
+
## 一直记得的 ← L3(从 DL3 排序选出的摘要),排最前
|
|
1181
|
+
## 最近沉淀的 ← L2
|
|
1182
|
+
## 刚发生的 ← L1,排最后,紧挨着你刚说的话
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
| 层 | 节流吗 | 为什么 |
|
|
1186
|
+
|---|---|---|
|
|
1187
|
+
| **L1** | 不节流 | 每轮都有新数据,随写随带;追加在末尾,前面缓存不受影响 |
|
|
1188
|
+
| **L2** | 不节流 | 同上 |
|
|
1189
|
+
| **L3** | **要** | 排在最前,它一变,后面 L2 / L1 的整段前缀缓存全部失效 |
|
|
1190
|
+
|
|
1191
|
+
L3 是从 DL3 按 `importance × heat` 排出来的,而 heat 随时间连续衰减,
|
|
1192
|
+
边界条目的分数此消彼长,入选集合时不时会变一条——不压住的话缓存会反复断。
|
|
1193
|
+
|
|
1194
|
+
所以 `refreshMinMs` 的语义从「整个记忆块的刷新间隔」改成
|
|
1195
|
+
**「L3 视图(DL3 排序)的重算间隔」**,默认仍是 30 分钟:
|
|
1196
|
+
|
|
1197
|
+
```js
|
|
1198
|
+
// memory5.mjs —— 全局唯一一处节流
|
|
1199
|
+
let l3Cache = null
|
|
1200
|
+
function l3View(now = tick(), opts = {}) {
|
|
1201
|
+
const refreshMs = Number(cfg.viewRefreshMs) || 0
|
|
1202
|
+
const key = cfg.longMaxChars // 容量变了立刻重算,不用等一个周期
|
|
1203
|
+
if (l3Cache && l3Cache.key === key && refreshMs > 0 && now - l3Cache.at < refreshMs) {
|
|
1204
|
+
return l3Cache.items
|
|
1205
|
+
}
|
|
1206
|
+
/* ...重算... */
|
|
1207
|
+
l3Cache = { at: now, key, items: out }
|
|
1208
|
+
}
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
**副作用(要知道)**:新沉淀进 DL3 的内容,最多延迟一个刷新周期才出现在 L3 里。
|
|
1212
|
+
想立刻生效就把 `refreshMinMs` 调小,或者改 `longMaxChars`(容量变化会立刻触发重算)。
|
|
1213
|
+
|
|
1214
|
+
实测:
|
|
1215
|
+
|
|
1216
|
+
```
|
|
1217
|
+
① 初始 L3: 3 条
|
|
1218
|
+
② 写入 L1 后立刻 buildBlock → L1 段 1 条(随写随带,不节流) ✅
|
|
1219
|
+
③ DL3 新增高优条目后 L3 保持不变(30 分钟节流生效) ✅
|
|
1220
|
+
④ viewRefreshMs = 0 时新条目立刻进 L3 ✅
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
### 想真正把上下文压下来,按收益排序
|
|
1224
|
+
|
|
1225
|
+
| 手段 | 效果 |
|
|
1226
|
+
|---|---|
|
|
1227
|
+
| **开新会话** | 最彻底。历史清零,上下文直接回到 1 万级 |
|
|
1228
|
+
| **少让工具全量打印** | 一条 `cat` 长文件就是几千字。用 `grep -c` / `head` / `-o` 只取需要的 |
|
|
1229
|
+
| 调小 `memoryCfg.longMaxChars` | 每条记忆快照的字数上限,从 4000 降到 2000 可省一半(代价:进上下文的记忆条数变少) |
|
|
1230
|
+
| 调大 `refreshMinMs` | 让 L3 视图更稳定(减少重算),默认已是 30 分钟。注意 L1/L2 不受它影响 |
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
## 17. 已知问题清单(待修,按发现时间倒序)
|
|
1235
|
+
|
|
1236
|
+
> 这里只记「确认存在但暂不修」的问题。修完的挪到对应章节的实施记录里。
|
|
1237
|
+
> 每条要写清楚:现象、怎么发现的、影响、可能的修法。
|
|
1238
|
+
|
|
1239
|
+
### K1 · action 通道一条都没写进 L1(2026-10-31 发现 → 同日已修)
|
|
1240
|
+
|
|
1241
|
+
**状态:✅ 已修复 2026-10-31 20:40。** 根因 + 修法见本节末尾。
|
|
1242
|
+
|
|
1243
|
+
**现象**:插件设计上每轮结束会写两类记忆——用户消息进 `channel: 'dialog'`,
|
|
1244
|
+
工具调用(`tool/call` + `tool/result` 配对)进 `channel: 'action'`。但实测 L1 的 9 条
|
|
1245
|
+
**全部是 `dialog`,`action` 零条**。
|
|
1246
|
+
|
|
1247
|
+
**怎么发现的**:
|
|
1248
|
+
|
|
1249
|
+
```bash
|
|
1250
|
+
node -e "
|
|
1251
|
+
const d=JSON.parse(require('fs').readFileSync('store.json','utf8'));
|
|
1252
|
+
const l1=d.items.filter(i=>i.layer==='L1');
|
|
1253
|
+
l1.forEach(i=>console.log(i.channel, i.text.length+'字', i.text.slice(0,40)))"
|
|
1254
|
+
# 输出 9 条全是 dialog:小鱼是你吗 / 艾婷门的群号是多少 / 照旧今天还没亲你呢…
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
**影响**:
|
|
1258
|
+
|
|
1259
|
+
- 「昨天帮你修过什么」这类**行为记忆**完全没有——只能靠会话历史,会话一关就没了
|
|
1260
|
+
- 跨会话恢复时模型不知道自己以前干过什么活
|
|
1261
|
+
- 反过来看,从省 token 角度这是好事(少记一堆命令输出);所以不急
|
|
1262
|
+
|
|
1263
|
+
**可能的原因**(未验证,按可能性排序):
|
|
1264
|
+
|
|
1265
|
+
1. `turn/end` 时 `buf.tools` 已经被清空——比如 `turn/end` 事件先于最后一个 `tool/result` 到达,
|
|
1266
|
+
或者某些步骤不发 `turn/end`
|
|
1267
|
+
2. `tool/call` / `tool/result` 的字段名与我用的 `d.callId` / `d.name` / `d.arguments` 不一致
|
|
1268
|
+
(dsh 版本差异)
|
|
1269
|
+
3. `writeMany` 对 `channel: 'action'` 的条目有过滤(比如重要性阈值),静默丢掉
|
|
1270
|
+
|
|
1271
|
+
**修法**:加一条审计——每次 `turn/end` 把「本轮 buffered 的 user 条数 / tool 条数 / 实际写入条数」
|
|
1272
|
+
写进 `audit.log`。有了这条日志,下次一看就知道是「没抓到」还是「抓到没写」。
|
|
1273
|
+
先加日志,别急着改逻辑。
|
|
1274
|
+
|
|
1275
|
+
### K1 修复记录(2026-10-31 20:40)
|
|
1276
|
+
|
|
1277
|
+
**根因**:`tool/result` 事件的 `callId` 和 `isError` **不在 `event.data` 顶层,而在 `event.data.message` 里**。
|
|
1278
|
+
|
|
1279
|
+
```js
|
|
1280
|
+
// 真实结构(从 session.v4.jsonl.zstd 里捞出来的)
|
|
1281
|
+
{ type: 'tool/result', data: {
|
|
1282
|
+
turn: 3, step: 1,
|
|
1283
|
+
message: {
|
|
1284
|
+
role: 'tool',
|
|
1285
|
+
toolCallId: 'call_00_...', // ← callId 在这里
|
|
1286
|
+
isError: false, // ← isError 也在这里
|
|
1287
|
+
content: [...] } } }
|
|
1288
|
+
|
|
1289
|
+
// 旧代码(永远拿到 undefined)
|
|
1290
|
+
const c = buf.tools.get(d.callId) // undefined
|
|
1291
|
+
c.err = !!d.isError // 恒为 false
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
匹配不上 → `c.done` 永远是 `false` → 被 `if (!c.done) continue` 静默跳过 → **action 零条,无任何报错**。
|
|
1295
|
+
|
|
1296
|
+
**修法**(`index.mjs` 的 `session/event` 处理器):
|
|
1297
|
+
|
|
1298
|
+
```js
|
|
1299
|
+
const cid = d.callId ?? d.message?.toolCallId ?? d.message?.source?.callId
|
|
1300
|
+
const c = cid ? buf.tools.get(cid) : undefined
|
|
1301
|
+
c.err = !!(d.isError ?? d.message?.isError)
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
同时改了两处行为:
|
|
1306
|
+
|
|
1307
|
+
1. **没配到 result 的调用也记**。原来 `if (!c.done) continue` 直接丢掉被中断的调用,
|
|
1308
|
+
现在记成「未完成:(被中断,无结果)」——用户要求「每次对话结束后记录确确实实进了 L1」,
|
|
1309
|
+
漏记等于打破这个保证。
|
|
1310
|
+
2. **加 `capture.log` 审计**。每轮结束追加一行到 `<数据目录>/capture.log`:
|
|
1311
|
+
`turn 归档 user=1 tool=3 rows=4 written=4 L1=4`。
|
|
1312
|
+
这类「静默丢数据」的 bug,只有把「抓到几条 / 写了几条 / 库里几条」摆在一起才看得见。
|
|
1313
|
+
|
|
1314
|
+
**验证**(模拟一轮含完成/失败/中断三种调用):
|
|
1315
|
+
|
|
1316
|
+
```
|
|
1317
|
+
dialog "帮我跑下测试"
|
|
1318
|
+
action "运行 bash「{"command":"npm test"}」→ 完成:3 个失败:test-m5"
|
|
1319
|
+
action "运行 read「{"file_path":"x.mjs"}」→ 失败:ENOENT: no such file"
|
|
1320
|
+
action "运行 grep「{"pattern":"foo"}」→ 未完成:(被中断,无结果)
|
|
1321
|
+
capture.log: turn 归档 user=1 tool=3 rows=4 written=4 L1=4
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
回归 41 + 20 + 17 全过。
|
|
1325
|
+
|
|
1326
|
+
**遗留**:`capture.log` 会一直增长,暂不轮转。等它大到碍事再说(一天几百行,无所谓)。
|
|
1327
|
+
|
|
1328
|
+
### K2 · 压缩阈值曾是 84 万 token(2026-10-31 已改配置,不算 bug)
|
|
1329
|
+
|
|
1330
|
+
不是代码问题,但记一笔:dsh 默认 `thresholdRatio: 0.8`(上下文窗口 1048576 的 80%),
|
|
1331
|
+
实际用不到那个量级,导致一条 4.3 万字工具输出永久滞留、每轮重读。
|
|
1332
|
+
已在 `~/.dsh/profiles/desktop/cordis.patch.yml` 覆盖为 `0.06` / `retainRatio: 0.03`,
|
|
1333
|
+
`tool-result-pruner` 的 `thresholdChars` 从 8192 降到 4000。详见第 16 节。
|
|
1334
|
+
|
|
1335
|
+
### 20:55 收紧到极限(1 万触发,只留 800 token)
|
|
1336
|
+
|
|
1337
|
+
用户要求「把 dsh 自带的会话记录删掉,历史全交给记忆系统」。配置改成:
|
|
1338
|
+
|
|
1339
|
+
```yaml
|
|
1340
|
+
- id: compaction-basic
|
|
1341
|
+
config:
|
|
1342
|
+
thresholdRatio: 0.01 # ≈1.05 万 token 就压
|
|
1343
|
+
retainTokens: 800 # 只留最近 800 token 原文
|
|
1344
|
+
headroomTokens: 4096 # 默认 65536 会把触发点推得很晚
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
**预期**:上下文从 ~1.6 万降到 ~0.8 万(系统 6k + 摘要 + 800 原文)。
|
|
1348
|
+
|
|
1349
|
+
**但有个账必须算清楚——LLM 压缩可能比不压缩更贵:**
|
|
1350
|
+
|
|
1351
|
+
```
|
|
1352
|
+
不压缩:每轮 1.6 万,绝大部分是缓存命中(约 1/10 价)
|
|
1353
|
+
≈ 0.0016 元/轮
|
|
1354
|
+
|
|
1355
|
+
压缩后:每轮 0.8 万 ≈ 0.0008 元/轮
|
|
1356
|
+
+ 每 4 轮一次压缩:读 1 万 + 写摘要
|
|
1357
|
+
约 0.0065 元/次 ÷ 4 ≈ 0.0016 元/轮
|
|
1358
|
+
─────────────────────────────────────────
|
|
1359
|
+
合计 ≈ 0.0024 元/轮
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
**结论:为了省钱而用 LLM 压缩是亏的**(贵约 50%)。压缩真正的价值是**防止上下文无限增长**(长会话会撞 104 万窗口),不是省钱。
|
|
1363
|
+
|
|
1364
|
+
要真正做到「零成本删历史」,只有一条路:**自定义 CompactionEngine**,压缩时不调模型,直接把选区归档进记忆再丢。
|
|
1365
|
+
|
|
1366
|
+
**为什么没直接做**(2026-10-31 结论):
|
|
1367
|
+
|
|
1368
|
+
1. `CompactionEngine` 是 `@deepseek-ai/dsh-compaction` 的抽象类,link 安装的插件 **import 不到**(同 `defineTool` / `schemastery` 那个坑)。
|
|
1369
|
+
绕法是用 `ctx.provide('compaction', 鸭子类型对象)`,理论上可行但未验证。
|
|
1370
|
+
| | |
|
|
1371
|
+
| --- | --- |
|
|
1372
|
+
2. **没有 dsh 运行环境可测**。压缩涉及 surface 选区、tool call/result 配对平衡、`compaction/start|end` 锁、失败回滚——
|
|
1373
|
+
写错会**直接损坏会话**(用户全部对话丢失)。这种代码不能"写完再说"。
|
|
1374
|
+
3. 需要先把 `compaction-basic` 在配置里 `disabled: true` 才能换实现,切换瞬间如果新实现有 bug,会话就停在那里压不动了。
|
|
1375
|
+
|
|
1376
|
+
**如果要继续做**,建议顺序:先在本地拨出一个一次性 dsh profile 做试验田,用固定 fixture 会话反复压,验证不损坏再上主 profile。
|
|
1377
|
+
|
|
1378
|
+
---
|
|
1379
|
+
|
|
1380
|
+
## 18. 历史压缩:插件接管,零 LLM(2026-10-31 实施)
|
|
1381
|
+
|
|
1382
|
+
> 前情:用户要的不是「压缩省钱」,是**上下文大小恒定**——每轮不调工具时 token 消耗应该差不多。
|
|
1383
|
+
> LLM 压缩做不到(摘要长度不可控 + 每次要一次模型调用),所以改成插件自己压。
|
|
1384
|
+
|
|
1385
|
+
### 机制
|
|
1386
|
+
|
|
1387
|
+
```
|
|
1388
|
+
turn/end 事件
|
|
1389
|
+
→ ① 本轮消息写进 L1(K1 修好后 user + action 都记)
|
|
1390
|
+
→ ② compactSurface():把除最近 N 轮以外的历史压成一条 20 字占位
|
|
1391
|
+
N = compactKeepTurns,**默认 0**(= 全部压掉,见下面的「默认完全取替」)
|
|
1392
|
+
├─ 先归档:被压掉的对话文本写进记忆(写失败就不压,宁大不丢)
|
|
1393
|
+
└─ 再替换:append 一条 user/message,surfaceOp = replace(startSeq, endSeq)
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
**dsh 自带的 LLM 压缩被推到 `thresholdRatio: 0.99`**(≈104 万 token),只当最后保险:
|
|
1397
|
+
万一本插件挂了,上下文涨到窗口 99% 时还能被 dsh 兜一层,不至于撞死。
|
|
1398
|
+
|
|
1399
|
+
### 为什么不用 `ctx.compaction` 接管
|
|
1400
|
+
|
|
1401
|
+
`CompactionEngine` 要求凑齐 `compaction/start` + 摘要节点 + `compaction/end` 三件套,
|
|
1402
|
+
返回完整的 `CompactionResult`,还要自己处理选区、tool 配对、并发锁、失败回滚。
|
|
1403
|
+
而它的摘要**必须调 LLM**(`summarizationProvider` / `summarizationModel` 是必填的)。
|
|
1404
|
+
|
|
1405
|
+
直接做 surface 替换只要 30 行,效果一样,而且零 LLM。**压完的历史不进模型,只进记忆。**
|
|
1406
|
+
|
|
1407
|
+
### 三个必须遵守的约束(都是实测撞出来的)
|
|
1408
|
+
|
|
1409
|
+
1. **`assistant/message` 不能带 `sourceEventSeqs`** —— 它内嵌 provider stream,dsh 直接抛错:
|
|
1410
|
+
`assistant/message embeds its source stream and cannot carry sourceEventSeqs`。
|
|
1411
|
+
所以替换节点只能用 `user/message`。
|
|
1412
|
+
2. **选区必须结束在 `user/message` 之前**。否则会把 `tool/call` 和 `tool/result` 切开,
|
|
1413
|
+
留下孤儿 tool result。按「user/message 的位置」切分天然安全。
|
|
1414
|
+
3. **占位本身也是 `user/message`,必须排除在轮次计数之外**。
|
|
1415
|
+
否则下一轮会把占位当成一轮又压一遍,无限生成新占位。判据:`source.kind === 'compaction'`。
|
|
1416
|
+
|
|
1417
|
+
### 配置
|
|
1418
|
+
|
|
1419
|
+
```json
|
|
1420
|
+
{
|
|
1421
|
+
"autoCompact": true, // 每轮结束自动压(默认开)
|
|
1422
|
+
"compactKeepTurns": 0, // 保留最近几轮原文。**默认 0 = 一条不留**
|
|
1423
|
+
"compactArchiveChars": 1200 // 每次归档进记忆的字数上限
|
|
1424
|
+
}
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
> 这三项 **2026-11-01 起不再出现在设置页**(用户决定):默认就是完全取替,没什么可调的;
|
|
1428
|
+
> 想多留点临时内容,把 L1 容量(`memoryCfg.hotMaxChars`)调大即可——L1 存的就是对话原话。
|
|
1429
|
+
> 要改仍然可以手改 `config.json`。
|
|
1430
|
+
|
|
1431
|
+
### 默认完全取替:`compactKeepTurns: 0`
|
|
1432
|
+
|
|
1433
|
+
`compactKeepTurns: 0` 现在是默认值:dsh 侧只剩 system + 一条占位,**历史完全由 L1 接管**。
|
|
1434
|
+
|
|
1435
|
+
它成立的前提是**三层恒定注入**(L1 必须始终在上下文里,否则模型看不见上一句)。
|
|
1436
|
+
这个前提在 `injectHot` 开关删除后已经恒定满足,不再需要用户手动配对——
|
|
1437
|
+
早先文档里那张「`injectHot` × `keepTurns` 配套表」就是因为这个才存在的,现已作废(见第 16 节)。
|
|
1438
|
+
|
|
1439
|
+
工作模式下想少压一点,见第 25 节(未实施)。
|
|
1440
|
+
|
|
1441
|
+
### 验证(本地 fixture,真 Session 对象,5 轮对话)
|
|
1442
|
+
|
|
1443
|
+
> 下面是 2026-10-31 的实测输出,当时 `compactKeepTurns = 1`。
|
|
1444
|
+
> 现在默认 0,surface 恒定只剩 2 条(见第 20 节末尾的补充)。
|
|
1445
|
+
|
|
1446
|
+
```
|
|
1447
|
+
第 1 轮后 surface 条数: 2
|
|
1448
|
+
第 2 轮后 surface 条数: 3 ← 从这开始恒定
|
|
1449
|
+
第 3 轮后 surface 条数: 3
|
|
1450
|
+
第 4 轮后 surface 条数: 3
|
|
1451
|
+
第 5 轮后 surface 条数: 3
|
|
1452
|
+
|
|
1453
|
+
最终可见内容:
|
|
1454
|
+
(此前 1 轮对话已归档到长期记忆,需要细节可以让我检索)
|
|
1455
|
+
第5轮问题
|
|
1456
|
+
第5轮回答
|
|
1457
|
+
|
|
1458
|
+
capture.log:
|
|
1459
|
+
turn 归档 user=1 tool=0 rows=1 written=1 L1=1
|
|
1460
|
+
压缩 removed=2 archived=1 ← 每轮都压,surface 不再增长
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
### 预期效果
|
|
1464
|
+
|
|
1465
|
+
```
|
|
1466
|
+
上下文 = 系统提示(≈6k) + 记忆块(≈1.5k) + 最近 1 轮 + 占位(20 字)
|
|
1467
|
+
≈ 固定 8k 上下,聊多少轮都不变
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
代价:模型对「更早的对话」只能靠记忆块 + `memory_search`,看不见逐字原文。
|
|
1471
|
+
|
|
1472
|
+
### 还没覆盖的
|
|
1473
|
+
|
|
1474
|
+
- **压缩后 assistant 的回复不进记忆**:turn 归档只记 user + tool,assistant 正文只在
|
|
1475
|
+
`compactSurface` 的归档里出现(作为「前 N 轮对话」的一部分)。如果哪天要「精确回忆我说过什么」,
|
|
1476
|
+
得把 assistant 正文也单独记一条。
|
|
1477
|
+
- **perf**:每轮压缩会 append 一条新事件,session 日志线性增长(磁盘),但 surface 恒定。
|
|
1478
|
+
|
|
1479
|
+
### 上线后撞到的三个 bug(2026-10-31 21:15 修,全部来自真实会话离线复现)
|
|
1480
|
+
|
|
1481
|
+
第一次上线后 `capture.log` 只有 `turn 归档`、**没有一行 `压缩`**——压缩静默失败了。
|
|
1482
|
+
用真实会话文件(`zstd -dc session.v4.jsonl.zstd` → `Session.create(id, events)`)离线复现,抓到三个:
|
|
1483
|
+
|
|
1484
|
+
**B1 · 选区起点撞上系统提示词**
|
|
1485
|
+
|
|
1486
|
+
```
|
|
1487
|
+
surface replace: node 0 holds the system prompt and may be rewritten
|
|
1488
|
+
only by a system/message over exactly that node
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
`surface.nodes[0]` 是 `system/message`,dsh 禁止 replace 覆盖它。
|
|
1492
|
+
**修法**:选区起点改成「第一个非 `system/message` 的节点」。
|
|
1493
|
+
|
|
1494
|
+
**B2 · `userIdx` 把框架注入算成了对话轮次**
|
|
1495
|
+
|
|
1496
|
+
`user/message` 有 **四种** 来源,只有第一种是真人:
|
|
1497
|
+
|
|
1498
|
+
| `source.kind` | 是什么 |
|
|
1499
|
+
|---|---|
|
|
1500
|
+
| `user` | 真人说话 ← 只算这个 |
|
|
1501
|
+
| `runtime-context` | 运行时快照(工作目录、沙箱策略、审批策略…) |
|
|
1502
|
+
| `skill-catalog` | 技能清单 |
|
|
1503
|
+
| `time-context` | 时间快照 |
|
|
1504
|
+
| `compaction` | 我们自己写的占位 |
|
|
1505
|
+
|
|
1506
|
+
旧代码只排除了 `compaction`,于是 6 轮对话被算成 **13 轮**,`keepTurns` 完全失真。
|
|
1507
|
+
**修法**:`if (src?.kind !== 'user') continue`。
|
|
1508
|
+
|
|
1509
|
+
**B3 · 归档把框架注入原样抄进了记忆(自我污染)**
|
|
1510
|
+
|
|
1511
|
+
归档时没过滤来源,结果 `runtime-context` 的全文(工作目录、沙箱策略)连同**注入块里的记忆块自身**
|
|
1512
|
+
一起被写进 L1——记忆把自己抄进了自己。而且它太长,一条就吃光 1200 字预算,
|
|
1513
|
+
导致真正的对话只剩一句「(还有 N 条略)」,L1 里实际躺着 3 条空归档。
|
|
1514
|
+
**修法**:归档只接受 `user` / `model` / `tool` 三种来源;超预算时改成「截断保留」而不是整条丢弃。
|
|
1515
|
+
|
|
1516
|
+
修完后同一份真实会话的归档内容:
|
|
1517
|
+
|
|
1518
|
+
```
|
|
1519
|
+
[归档] 前 5 轮对话(已被压缩出上下文):
|
|
1520
|
+
user: 嗯,简单回复我下,我看看现在会不会无缘无故消耗许多token
|
|
1521
|
+
bot: 没事,我就简单回你一句,不调工具、不查东西…
|
|
1522
|
+
user: 「阿里云送鸡蛋那次,谁领了券?」
|
|
1523
|
+
bot: [调用 memory_search …] [调用 memory_search …]
|
|
1524
|
+
tool: - [lm-41] (DL3 2026/10/29) 昨晚豪豪星猫发了阿里云送鸡蛋的图…
|
|
1525
|
+
…(另有 17 条系统注入或过长内容未收录)
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
### 真实的 token 构成(用户实测每轮 20k,2026-10-31)
|
|
1529
|
+
|
|
1530
|
+
| 成分 | token | 说明 |
|
|
1531
|
+
|---|---|---|
|
|
1532
|
+
| 系统提示词 | ≈4.5k | 5775 字 |
|
|
1533
|
+
| **工具 schema** | **≈7.5k** | 13 个工具的 JSON 定义,**最大单项,且插件管不了** |
|
|
1534
|
+
| 记忆块 | ≈3k | L3 视图 3952 字 |
|
|
1535
|
+
| 对话历史 | 5k+ | 随轮数增长 → 压缩后降到 ≈100 |
|
|
1536
|
+
|
|
1537
|
+
**所以压缩后的地板是 ≈15k,不是我之前估的 8k。** 之前把 5775 字按 4k token 算,
|
|
1538
|
+
忽略了工具 schema 这 7.5k。要再往下压只能减工具(dsh 侧配置)或缩记忆块。
|
|
1539
|
+
|
|
1540
|
+
### 21:35 去掉总闸门 bootMaxChars(限制只由三层容量决定)
|
|
1541
|
+
|
|
1542
|
+
用户的设计意图:**上下文大小的唯一约束应该是 L1/L2/L3 各自的容量**,不该再有一个独立的总额。
|
|
1543
|
+
|
|
1544
|
+
旧逻辑:`budget(4200)` 当总闸门,L3 先吃(`pack(..., Infinity)`),剩下 `rest` 分给 L2/L1。
|
|
1545
|
+
实测后果:L3 吃掉 4067 字,L2 只剩 133 字预算、**351 字里只进了 55 字**——
|
|
1546
|
+
「刚沉淀的对话」优先级还不如「一周前的群聊」,明显不对。
|
|
1547
|
+
|
|
1548
|
+
新逻辑:
|
|
1549
|
+
|
|
1550
|
+
```js
|
|
1551
|
+
const longLines = pack(visible(long), Infinity) // 上限 = longMaxChars(l3View 已截)
|
|
1552
|
+
const consLines = pack(visible(cons), Infinity) // 上限 = consolidateMaxChars(满了会溢出)
|
|
1553
|
+
const hotLines = includeHot ? pack(visible(hot), Infinity) : [] // 上限 = hotMaxChars
|
|
1554
|
+
// budget 退化为可选的硬上限:0 = 不限(默认),> 0 才截断
|
|
1555
|
+
if (budget > 0 && out.length > budget) return out.slice(0, budget)
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
效果(真实库):
|
|
1559
|
+
|
|
1560
|
+
| | 改造前 | 改造后 |
|
|
1561
|
+
|---|---|---|
|
|
1562
|
+
| 总注入 | 4139 字 | **4625 字**(≈3.1k token) |
|
|
1563
|
+
| L3 | 4067 字 | 4150 字 |
|
|
1564
|
+
| L2 | **55 字** | **458 字(全进)** |
|
|
1565
|
+
|
|
1566
|
+
代价仅 +486 字(≈+320 token),换来「刚聊的事」能完整进上下文。
|
|
1567
|
+
|
|
1568
|
+
三层容量对照(默认值,都在 `DEFAULTS`):
|
|
1569
|
+
|
|
1570
|
+
| 层 | 参数 | 默认 | 当前配置 |
|
|
1571
|
+
|---|---|---|---|
|
|
1572
|
+
| L1 | `hotMaxChars` | 2000 | — |
|
|
1573
|
+
| L2 | `consolidateMaxChars` | 2500 | — |
|
|
1574
|
+
| L3 | `longMaxChars` | 2000 | 4000 |
|
|
1575
|
+
|
|
1576
|
+
想控制上下文大小就调这三个(或 config.json 的 `memoryCfg`),不要再用 `bootMaxChars`。
|
|
1577
|
+
|
|
1578
|
+
---
|
|
1579
|
+
|
|
1580
|
+
## 19. dsh 适配层速查(换 agent 时看这节)
|
|
1581
|
+
|
|
1582
|
+
> 这一节把所有「dsh 特有的东西」集中列出来。移植到别的 agent 时,只需要替换本节提到的接口,
|
|
1583
|
+
> `memory5.mjs`(六层记忆)和 `compaction.mjs` 的选区逻辑都不用动。
|
|
1584
|
+
|
|
1585
|
+
### 19.1 插件形态
|
|
1586
|
+
|
|
1587
|
+
```
|
|
1588
|
+
package.json
|
|
1589
|
+
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } } ← 告诉 dsh 怎么插进配置树
|
|
1590
|
+
"files": [...] ← 漏一个文件,装上就 ERR_MODULE_NOT_FOUND
|
|
1591
|
+
cordis.patch.yml
|
|
1592
|
+
- insert: [{ id: memory-tiers, name: dsh-memory-tiers }] ← id 随便取,name 必须等于包名
|
|
1593
|
+
index.mjs
|
|
1594
|
+
export const name = 'memory-tiers'
|
|
1595
|
+
export const inject = ['tools'] ← 等这些服务就绪才加载
|
|
1596
|
+
export function apply(ctx, config) {} ← dsh 启动时调一次
|
|
1597
|
+
```
|
|
1598
|
+
|
|
1599
|
+
安装:`pnpm add "link:/path"`(开发)或 `pnpm add xxx.tgz`(分发),然后把包名加进
|
|
1600
|
+
`~/.dsh/profiles/<profile>/package.json` 的 `dsh.profile.bundles`,**重启生效**。
|
|
1601
|
+
装错 profile 是静默失败——插件不会加载,也没有任何报错(踩过)。
|
|
1602
|
+
|
|
1603
|
+
### 19.2 用到的 ctx 能力
|
|
1604
|
+
|
|
1605
|
+
| 能力 | 用途 | 备注 |
|
|
1606
|
+
|---|---|---|
|
|
1607
|
+
| `ctx.tools.register({name, description, parameters, execute})` | 注册 5 个记忆工具 | 参数是手写 JSON Schema,不能用 `defineTool`(import 不到) |
|
|
1608
|
+
| `ctx.systemPrompt.context({name, order, text, interpolate})` | 注入记忆块 | **用 context 不用 section**,原因见第 16 节 |
|
|
1609
|
+
| `ctx.commands.register({name, ...})` | `/memory` 命令 | |
|
|
1610
|
+
| `ctx.on('session/event', (session, event) => {})` | 捕获对话 | 返回 disposer |
|
|
1611
|
+
| `ctx.effect(fn)` | 注册定时器 | `fn` 返回清理函数,否则卸载后泄漏 |
|
|
1612
|
+
| `ctx.inject(['systemPrompt'], cb)` | 延迟拿服务 | 服务可能比插件晚就绪 |
|
|
1613
|
+
| `ctx.logger` | 日志 | 可能不存在,要有 console 兜底 |
|
|
1614
|
+
|
|
1615
|
+
### 19.3 `session/event` 事件与字段(字段坑最费时间)
|
|
1616
|
+
|
|
1617
|
+
| 事件类型 | `data` 关键字段 |
|
|
1618
|
+
|---|---|
|
|
1619
|
+
| `user/message` | `{ role, content, source }` |
|
|
1620
|
+
| `assistant/message` | `{ turn, step, message, usage, stream }`,`message.content` 含 `text` / `reasoning` / `tool_call` 块 |
|
|
1621
|
+
| `tool/call` | `{ turn, step, callId, name, arguments }` ← **callId 在顶层** |
|
|
1622
|
+
| `tool/result` | `{ turn, step, message }`,**callId 和 isError 在 `message` 里**,不在顶层 |
|
|
1623
|
+
| `turn/start` / `turn/end` | `{ turn }` |
|
|
1624
|
+
|
|
1625
|
+
> ⚠️ `tool/result` 的 `d.callId` 恒为 `undefined`。正确写法:
|
|
1626
|
+
> `d.callId ?? d.message?.toolCallId ?? d.message?.source?.callId`。
|
|
1627
|
+
> 读错字段的后果是**所有工具调用被静默丢弃,一句报错都没有**(K1)。
|
|
1628
|
+
|
|
1629
|
+
`user/message` 的 `source.kind` 有五种,只有第一种是真人:
|
|
1630
|
+
|
|
1631
|
+
| kind | 含义 |
|
|
1632
|
+
|---|---|
|
|
1633
|
+
| `user` | 真人输入 |
|
|
1634
|
+
| `runtime-context` | 运行时快照(工作目录/沙箱/审批策略),**每轮都注入** |
|
|
1635
|
+
| `skill-catalog` | 技能清单 |
|
|
1636
|
+
| `time-context` | 时间快照 |
|
|
1637
|
+
| `compaction` | 压缩占位 |
|
|
1638
|
+
|
|
1639
|
+
过滤错会把框架注入当成对话(实测 6 轮被算成 13 轮),甚至把注入块抄进记忆形成自我污染。
|
|
1640
|
+
|
|
1641
|
+
### 19.4 surface(模型看得见的那部分历史)
|
|
1642
|
+
|
|
1643
|
+
```js
|
|
1644
|
+
session.surface.nodes // readonly SessionSeq[],模型可见顺序
|
|
1645
|
+
session.eventAt(seq) // → SessionEvent | undefined
|
|
1646
|
+
session.seq // 当前最大 seq
|
|
1647
|
+
session.append(type, data, { // 追加 / 替换
|
|
1648
|
+
surfaceOp: 'append' | { op: 'replace', startSeq, endSeq },
|
|
1649
|
+
sourceEventSeqs: [...], // ★ assistant/message 禁止带这个
|
|
1650
|
+
})
|
|
1651
|
+
```
|
|
1652
|
+
|
|
1653
|
+
三条硬规则:
|
|
1654
|
+
|
|
1655
|
+
1. **`assistant/message` 不能带 `sourceEventSeqs`**(内嵌 provider stream,dsh 直接抛错)
|
|
1656
|
+
→ 压缩的替换节点只能用 `user/message`。
|
|
1657
|
+
2. **不能 replace 系统提示词节点**:`node 0 holds the system prompt and may be rewritten
|
|
1658
|
+
only by a system/message over exactly that node` → 选区起点必须跳过开头的 `system/message`。
|
|
1659
|
+
3. **`startSeq`/`endSeq` 是 surface 位置,不是数值区间**——多次替换后高位 seq 会落到老位置上。
|
|
1660
|
+
|
|
1661
|
+
### 19.5 怎么拿到 dsh 的包(`@deepseek-ai/*`)
|
|
1662
|
+
|
|
1663
|
+
link 安装的插件 **import 不到** dsh 自己的包,三个解法:
|
|
1664
|
+
|
|
1665
|
+
| 场景 | 方法 |
|
|
1666
|
+
|---|---|
|
|
1667
|
+
| 本地测试 | 绝对路径:`import('file:///…/npm-cache/_npx/<hash>/node_modules/@deepseek-ai/dsh-session/lib/index.js')`。包内部依赖会正常解析。`NODE_PATH` 对 ESM **无效** |
|
|
1668
|
+
| 运行时拿基类 | 沿原型链偷:`Object.getPrototypeOf(ctx.compaction)` 上溯找 `constructor.name === 'CompactionEngine'` |
|
|
1669
|
+
| 想要正经依赖 | 软链接插件目录的 `node_modules/@deepseek-ai` → npx 缓存,但 npx 路径带 hash,dsh 升级就断 |
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
**本插件最终的取舍:一个都不用。** 压缩改成直接做 `session.append` 的 surface 替换,
|
|
1673
|
+
不继承任何 dsh 类,也不 import 任何 dsh 包——所以它在任何提供相同 `session` API 的宿主上都能跑。
|
|
1674
|
+
|
|
1675
|
+
### 19.6 数据目录(可指定,便于备份与迁移)
|
|
1676
|
+
|
|
1677
|
+
优先级(高 → 低):
|
|
1678
|
+
|
|
1679
|
+
1. 插件配置的 `dir`(`apply(ctx, { dir })`,dsh 侧 cordis 配置)
|
|
1680
|
+
2. 环境变量 **`MEMORY_TIERS_DIR`**(支持 `~` 展开)
|
|
1681
|
+
3. `<DSH_HOME 或 ~/.dsh>/storages/memory-v5`
|
|
1682
|
+
|
|
1683
|
+
**不能从 config.json 读**——config.json 就在目录里,先有目录才能读它。
|
|
1684
|
+
|
|
1685
|
+
目录是**自包含**的,整个拷走就能在别处恢复:
|
|
1686
|
+
|
|
1687
|
+
```
|
|
1688
|
+
<dir>/
|
|
1689
|
+
store.json 全部记忆条目(含分层、importance、heat、时间戳)
|
|
1690
|
+
vectors-part-0000.json 向量分片(每片 5000 条)
|
|
1691
|
+
archive/ 每条记忆的 .md 归档
|
|
1692
|
+
graveyard/ 遗忘/作废的台账
|
|
1693
|
+
config.json 参数(apiKey / 三层容量 / 压缩开关)
|
|
1694
|
+
status.json 最近一次加载状态(loadedAt / promptInjected / semantic)
|
|
1695
|
+
capture.log 每轮归档与压缩审计
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
### 19.7 移植到别的 agent:改动清单
|
|
1699
|
+
|
|
1700
|
+
| 要改的地方 | 现在(dsh) | 别的 agent 要提供什么 |
|
|
1701
|
+
|---|---|---|
|
|
1702
|
+
| 写入触发 | `ctx.on('session/event')` 的 `turn/end` | 任意「一轮结束」钩子,每轮调 `mem.writeMany(rows)` |
|
|
1703
|
+
| 读取注入 | `ctx.systemPrompt.context()` | 能把一段文本作为 user 快照插进历史,且内容不变时不重复插 |
|
|
1704
|
+
| 历史压缩 | `session.surface.nodes` + `append(replace)` | 能列出可见消息、能用一条消息替换一段 |
|
|
1705
|
+
| 工具 | `ctx.tools.register()` | 任意 function calling 注册方式 |
|
|
1706
|
+
| 定时维护 | `ctx.effect(setInterval)` | 任意定时器 |
|
|
1707
|
+
|
|
1708
|
+
**`memory5.mjs` 一行都不用改**——它只依赖 `{ text, speaker, channel, at }` 这种纯数据。
|
|
1709
|
+
`compaction.mjs` 只依赖 `session.surface.nodes` / `eventAt` / `append` 三个方法,
|
|
1710
|
+
适配层写个小 shim 就能复用。
|
|
1711
|
+
|
|
1712
|
+
---
|
|
1713
|
+
> **分区 D · 插件化落地**(20–24 节)— 设置页、模型配置、向量运维
|
|
1714
|
+
|
|
1715
|
+
---
|
|
1716
|
+
|
|
1717
|
+
## 20. 设置页:DSH「设置 → 记忆」(2026-11-01,v0.4.0)
|
|
1718
|
+
|
|
1719
|
+
> 之前只能在 `~/.dsh/storages/memory-v5/config.json` 里手改,改错一个逗号插件就起不来。
|
|
1720
|
+
> 现在有了图形界面,12 个字段分 5 组,外加只读运行状态。
|
|
1721
|
+
|
|
1722
|
+
### 为什么手写界面,不用 schemastery
|
|
1723
|
+
|
|
1724
|
+
dsh 的自动表单要 `@deepseek-ai/schemastery` 声明 schema,**link 安装的插件 import 不到**
|
|
1725
|
+
(同 `defineTool` / `CompactionEngine` 那个坑,见 19.5)。所以走「自己开 HTTP 接口 + 自己写 React 组件」:
|
|
1726
|
+
|
|
1727
|
+
```
|
|
1728
|
+
package.json
|
|
1729
|
+
"dsh": { "client": { "platform": "web", "inject": [] } }
|
|
1730
|
+
"exports": { "./client": "./client.mjs" }
|
|
1731
|
+
|
|
1732
|
+
client.mjs 浏览器端:React section
|
|
1733
|
+
settings.mjs 服务端:/dsh-memory/api/*
|
|
1734
|
+
```
|
|
1735
|
+
|
|
1736
|
+
### 客户端:三个关键点
|
|
1737
|
+
|
|
1738
|
+
```js
|
|
1739
|
+
window.__ModuleLoader__.load({
|
|
1740
|
+
id: 'dsh-memory-tiers',
|
|
1741
|
+
factory: (require) => {
|
|
1742
|
+
const React = require('react') // 宿主提供的 react
|
|
1743
|
+
const { createElement: h } = React
|
|
1744
|
+
// ...组件...
|
|
1745
|
+
exports.apply = apply
|
|
1746
|
+
return module.exports
|
|
1747
|
+
},
|
|
1748
|
+
})
|
|
1749
|
+
|
|
1750
|
+
const inject = ['slots']
|
|
1751
|
+
function apply(ctx) {
|
|
1752
|
+
ctx.effect(() => ctx.slots.inject('settings.section', () =>
|
|
1753
|
+
ctx.slots.register(
|
|
1754
|
+
{ name: 'settings.section', id: 'memory-tiers', order: 7, label: () => '记忆' },
|
|
1755
|
+
MemoryTiersSection,
|
|
1756
|
+
)))
|
|
1757
|
+
}
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
- 模块不是标准 ESM,是 **`window.__ModuleLoader__.load({id, factory})`**(照抄 dsh-bili-whale)
|
|
1761
|
+
- `require('react')` 由宿主注入,**不需要打包工具**(手写 47KB 也能跑,没有 devDependencies)
|
|
1762
|
+
- CSS 通过 `<style data-dmt=…>` 注入,颜色用 `--dsw-alias-*` 变量,深浅色主题自适应
|
|
1763
|
+
|
|
1764
|
+
`order: 7` 排在养鱼(6)后面。`label()` 是函数,支持后续改文案不用重载。
|
|
1765
|
+
|
|
1766
|
+
### 服务端:一个路径只能注册一个 handler
|
|
1767
|
+
|
|
1768
|
+
```js
|
|
1769
|
+
ctx.webServer.register({ kind: 'exact', path: '/dsh-memory/api/settings', handler })
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
> ⚠️ **踩过的坑**:读和写注册成两个 handler(同一路径两次),宿主「首个匹配即终止」,
|
|
1773
|
+
> 结果 GET 200 而 POST 405。必须**合成一个 handler,内部按 `req.method` 分派**。
|
|
1774
|
+
> 这个坑 dsh-bili-whale 的源码注释里提过,我们还是踩了一遍。
|
|
1775
|
+
|
|
1776
|
+
字段写入走**白名单**(`WRITABLE` 表),前端传 `dir`、`evil` 都会被忽略——
|
|
1777
|
+
配置目录不能让前端改,否则等于开了任意路径写文件。
|
|
1778
|
+
|
|
1779
|
+
### 界面结构
|
|
1780
|
+
|
|
1781
|
+
> 下表是**当前**结构(2026-11-01 又改过一轮,见本节末尾)。
|
|
1782
|
+
> 本节讲的是**机制**(怎么挂载、怎么写配置),那部分从 v0.4.0 起没变过。
|
|
1783
|
+
|
|
1784
|
+
| 分组 | 字段 |
|
|
1785
|
+
|---|---|
|
|
1786
|
+
| 每层现在存了多少(只读) | 六层各**条数 + 字数 + 这层干什么**,进上下文的层打标签;向量数、数据目录 |
|
|
1787
|
+
| 嵌入与重排 | 预设二选一(硅基流动免费 / 自己指定)→ `apiKey`;选"自己指定"才展开 `apiBase`、`models.embed.*`、`models.rerank.*` |
|
|
1788
|
+
| 融合与摘要 | 每个阶段二选一(跟随当前会话模型 / 自己指定)+ `noThink` 开关 |
|
|
1789
|
+
| 三层容量 | `memoryCfg.hotMaxChars` / `consolidateMaxChars` / `longMaxChars` / `refreshMinMs` |
|
|
1790
|
+
| 写入捕获 | `userCaptureChars`、`toolCaptureChars` |
|
|
1791
|
+
| 底部 | `capture.log` 最后 15 行(每轮的归档与压缩记录,只读) |
|
|
1792
|
+
|
|
1793
|
+
**两处刻意不暴露**(2026-11-01 用户决定):
|
|
1794
|
+
|
|
1795
|
+
1. **没有「是否注入 L1」开关**。三层恒定注入,可调的是各自容量——
|
|
1796
|
+
`injectHot` 配置项已删除(代码不再读取)。早先"L1 每轮变会打掉缓存"的理由不成立,
|
|
1797
|
+
见第 16 节「记忆刷新:只有 L3 需要节流」。
|
|
1798
|
+
2. **没有「历史压缩」分组**。默认就是**完全取替** dsh 会话历史(`compactKeepTurns: 0`)——
|
|
1799
|
+
L1 里已经有历史了。想多留点临时内容,把 L1 容量调大即可。
|
|
1800
|
+
压缩仍在跑,只是不再需要调:`autoCompact` 默认开,零 LLM,被压掉的内容先归档进记忆。
|
|
1801
|
+
3. **API Key 放在「嵌入与重排」下面**,不在顶部单独一组。因为它只服务嵌入与重排——
|
|
1802
|
+
融合与摘要默认跟着当前会话模型走,根本不需要 key。选"硅基流动免费"时只显示 Key,
|
|
1803
|
+
选"自己指定来源"才展开地址和模型名。
|
|
1804
|
+
|
|
1805
|
+
### 改配置需要重启
|
|
1806
|
+
|
|
1807
|
+
保存只写 `config.json`,运行中的 `mem` 实例不会重建。面板上明确写了「重启 dsh 后生效」。
|
|
1808
|
+
|
|
1809
|
+
要做热重载的话,得在保存接口里 dispose 旧 `mem` 再 `createMemory5` 一遍,
|
|
1810
|
+
但内存里的分层状态、队列、定时器都要跟着重建,风险大于收益——**现阶段就要求重启**。
|
|
1811
|
+
|
|
1812
|
+
### 验证
|
|
1813
|
+
|
|
1814
|
+
> 下面是 **v0.4.0 时的输出**(当时有 `injectHot`、12 字段 5 分组)。
|
|
1815
|
+
> 现在的字段结构见上面的「界面结构」表,模型部分见第 24 节。
|
|
1816
|
+
|
|
1817
|
+
```
|
|
1818
|
+
注册的路由: /dsh-memory/api/settings:exact, /dsh-memory/api/log:exact
|
|
1819
|
+
|
|
1820
|
+
GET → HTTP 200, config: apiKey/bootMaxChars/injectHot/memoryCfg
|
|
1821
|
+
live: {counts:{L1:0,L2:0,L3:0,DL1:0,DL2:0,DL3:0}, vectors:0, semantic:true}
|
|
1822
|
+
POST → HTTP 200, applied: ["apiKey","injectHot","memoryCfg.hotMaxChars","memoryCfg.longMaxChars"]
|
|
1823
|
+
ignored: [] needRestart: true
|
|
1824
|
+
落盘正确;传 dir / evil 被白名单挡住
|
|
1825
|
+
|
|
1826
|
+
客户端: 模块加载 → 导出 apply/inject/MemoryTiersSection/FIELDS
|
|
1827
|
+
12 个字段 / 5 个分组
|
|
1828
|
+
slots.register({name:'settings.section', id:'memory-tiers', order:7, label:'记忆'})
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
### 21:00 补充:`compactKeepTurns: 0` 是受支持的显式选项(2026-11-01)
|
|
1832
|
+
|
|
1833
|
+
用户要的是「dsh 历史一条不留,全部交给记忆」。实测**能做**:
|
|
1834
|
+
|
|
1835
|
+
```
|
|
1836
|
+
第 1 轮压缩: removed=2 | surface 剩 2 条
|
|
1837
|
+
第 2 轮压缩: removed=3 | surface 剩 2 条
|
|
1838
|
+
第 3 轮压缩: removed=3 | surface 剩 2 条
|
|
1839
|
+
第 4 轮压缩: removed=3 | surface 剩 2 条
|
|
1840
|
+
|
|
1841
|
+
最终 surface:
|
|
1842
|
+
system/message: sys
|
|
1843
|
+
user/message: (此前 1 轮对话已归档到长期记忆,需要细节可以让我检索)
|
|
1844
|
+
```
|
|
1845
|
+
|
|
1846
|
+
聊多少轮,dsh 侧都只剩 **system + 一条占位**。
|
|
1847
|
+
|
|
1848
|
+
**以前这段是靠巧合工作的**:`userIdx[length - 0]` 得到 `undefined`,`slice(first, undefined)`
|
|
1849
|
+
恰好等于「取到末尾」。虽然结果对,但纯属运气——已改成显式分支:
|
|
1850
|
+
|
|
1851
|
+
```js
|
|
1852
|
+
const keepStart = keepTurns === 0 ? nodes.length : userIdx[userIdx.length - keepTurns]
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
**前提是三层恒定注入**(L1 必须在上下文里,否则模型看不见上一句)。
|
|
1856
|
+
`injectHot` 开关已在 v0.6.0 删除,现在是恒定行为,不再需要手动配对(见第 16 节 ②)。
|
|
1857
|
+
|
|
1858
|
+
注入块对照(真实库):
|
|
1859
|
+
|
|
1860
|
+
| 配置 | 注入块 | 构成 |
|
|
1861
|
+
|---|---|---|
|
|
1862
|
+
| 不含 L1(旧默认,开关已删) | 4625 字 ≈ 3083 token | L3 4150 + L2 458 |
|
|
1863
|
+
| **含 L1(现在恒定如此)** | 6088 字 ≈ 4059 token | L3 4150 + L2 458 + L1 1455 |
|
|
1864
|
+
|
|
1865
|
+
每轮多约 1000 token(追加新快照,全价)——相对于「dsh 历史归零」这点代价可以接受。
|
|
1866
|
+
|
|
1867
|
+
配置改动落在 `~/.dsh/storages/memory-v5/config.json`:
|
|
1868
|
+
|
|
1869
|
+
```json
|
|
1870
|
+
{ "compactKeepTurns": 0 } // injectHot 已删除,三层恒定注入
|
|
1871
|
+
```
|
|
1872
|
+
|
|
1873
|
+
---
|
|
1874
|
+
|
|
1875
|
+
## 21. 模型分阶段配置(2026-11-01,v0.5.0)
|
|
1876
|
+
|
|
1877
|
+
> 背景:原来四个模型角色(向量 / 重排 / 轻任务 / 摘要)**共用一套 `apiKey` + `apiBase`**,
|
|
1878
|
+
> 设置页也没暴露,等于绑死在硅基流动上。现在每个阶段都能单独指定 `{ base, apiKey, model }`。
|
|
1879
|
+
|
|
1880
|
+
### 四个阶段
|
|
1881
|
+
|
|
1882
|
+
| 阶段 | 干什么 | 调用频率 | 默认模型 |
|
|
1883
|
+
|---|---|---|---|
|
|
1884
|
+
| `embed` | 向量化,检索和入库判重的基础 | 每条记忆 1 次 | `BAAI/bge-m3` |
|
|
1885
|
+
| `rerank` | 检索结果精排 | 每次检索 | `BAAI/bge-reranker-v2-m3` |
|
|
1886
|
+
| `chat` | 判重、判矛盾、分类等高频小任务 | 很频繁 | 默认**跟随当前会话模型**(HTTP 时 `inclusionAI/Ling-mini-2.0`)|
|
|
1887
|
+
| `summary` | 把一批对话压成摘要 | 低频但重 | 默认**跟随当前会话模型**(HTTP 时 `deepseek-ai/DeepSeek-V4.1-Flash`)|
|
|
1888
|
+
|
|
1889
|
+
> **v0.6.0 起默认值改了两处**(见第 24 节):
|
|
1890
|
+
> ① embed / rerank 从 Qwen3 那对换成 **BAAI 那对**——硅基流动上免费、中文够用,
|
|
1891
|
+
> 且这对是同一家同代际的配套组合;换 embed 记得跑一次 `node reindex.mjs`(见第 22 节)。
|
|
1892
|
+
> ② chat / summary 默认 `source: session`,**直接用当前对话的模型**,不需要自己的 key。
|
|
1893
|
+
|
|
1894
|
+
### 解析规则
|
|
1895
|
+
|
|
1896
|
+
```js
|
|
1897
|
+
function stage(name) {
|
|
1898
|
+
const over = cfg.models[name] || {}
|
|
1899
|
+
const base = over.base || cfg.base // 没覆盖就用顶层
|
|
1900
|
+
const apiKey = over.apiKey || cfg.apiKey
|
|
1901
|
+
const model = over.model || cfg[name + 'Model'] // 再退到默认模型名
|
|
1902
|
+
return { base, apiKey, model, chat: base + '/chat/completions', embed: …, rerank: … }
|
|
1903
|
+
}
|
|
1904
|
+
```
|
|
1905
|
+
|
|
1906
|
+
**优先级:`models[stage].x` > 顶层 `cfg.x` > 该阶段默认模型名。**
|
|
1907
|
+
空字符串是「删除这条覆盖、回落到顶层」,不是「设成空值」——这样前端不用区分“没填过”和“清空了”。
|
|
1908
|
+
|
|
1909
|
+
配置示例(摘要单独走 DeepSeek 官方,其余留硅基流动):
|
|
1910
|
+
|
|
1911
|
+
```json
|
|
1912
|
+
{
|
|
1913
|
+
"apiKey": "<你的硅基流动 Key>",
|
|
1914
|
+
"apiBase": "https://api.siliconflow.cn/v1",
|
|
1915
|
+
"models": {
|
|
1916
|
+
"summary": {
|
|
1917
|
+
"base": "https://api.deepseek.com",
|
|
1918
|
+
"apiKey": "sk-deepseek",
|
|
1919
|
+
"model": "deepseek-chat"
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
```
|
|
1924
|
+
|
|
1925
|
+
### 实现要点
|
|
1926
|
+
|
|
1927
|
+
- `chat()` 多了一个 `stage` 参数,内部用该阶段的 url 和 key 发请求——**摘要能挂到另一家**
|
|
1928
|
+
- `embed` / `rerank` 各自调 `stage('embed')` / `stage('rerank')`,不再直接用全局 `cfg.apiKey`
|
|
1929
|
+
- key 检查放宽:`cfg.apiKey || 任一 models[s].apiKey`,否则只配了摘要一家时会被误判为"没配"
|
|
1930
|
+
- `silicon.resolveStage(name)` 和 `silicon.stages()` 暴露给设置页,显示**实际生效**的值(覆盖已合并)
|
|
1931
|
+
|
|
1932
|
+
### 验证
|
|
1933
|
+
|
|
1934
|
+
> 下面是 **v0.5.0 时的输出**,当时 rerank 默认还是 Qwen3 那对。
|
|
1935
|
+
> v0.6.0 起默认已改成 BAAI 那对(见本节开头的表格)。
|
|
1936
|
+
|
|
1937
|
+
```
|
|
1938
|
+
顶层硅基流动 + summary 单独挂 deepseek + embed 换 bge-m3:
|
|
1939
|
+
embed {"base":"…siliconflow.cn/v1","model":"BAAI/bge-m3","hasKey":true}
|
|
1940
|
+
rerank {"base":"…siliconflow.cn/v1","model":"Qwen/Qwen3-Reranker-0.6B","hasKey":true}
|
|
1941
|
+
chat {"base":"…siliconflow.cn/v1","model":"inclusionAI/Ling-mini-2.0","hasKey":true}
|
|
1942
|
+
summary {"base":"https://api.deepseek.com","model":"deepseek-chat","hasKey":true}
|
|
1943
|
+
|
|
1944
|
+
设置页写入 → applied: ["models.summary.base","models.summary.apiKey","models.summary.model"]
|
|
1945
|
+
清空 base → 该键被删除,回落顶层(不是留一个空字符串)
|
|
1946
|
+
全空 → 抛「缺少 apiKey(顶层或某个阶段的 models[s].apiKey)」
|
|
1947
|
+
```
|
|
1948
|
+
|
|
1949
|
+
### 一个提醒
|
|
1950
|
+
|
|
1951
|
+
换 `embed` 模型会导致**向量维度变化**,旧库的向量全部作废。真要换得清空 `vectors-part-*.json` 并重建,
|
|
1952
|
+
否则检索结果会乱。设置页的提示里写了这条。
|
|
1953
|
+
|
|
1954
|
+
---
|
|
1955
|
+
|
|
1956
|
+
## 22. 向量指纹与重建(2026-11-01,v0.5.1)
|
|
1957
|
+
|
|
1958
|
+
> 起因:想换 `BAAI/bge-m3` 做 embedding。查了一下发现两件事——
|
|
1959
|
+
> ① 现在根本没连模型(`apiKey` 为空,82 条向量是本地 `hashEmbed` 出来的);
|
|
1960
|
+
> ② 当时的本地哈希向量 / Qwen3-Embedding-0.6B / bge-m3 **全是 1024 维**
|
|
1961
|
+
> (哈希向量已在 v0.5.2 删除,见第 23 节;但「同维度分不清」这个问题本身依然存在),
|
|
1962
|
+
> `cosine()` 里的 `a.length !== b.length` 永远不触发。
|
|
1963
|
+
|
|
1964
|
+
### 静默污染问题
|
|
1965
|
+
|
|
1966
|
+
```js
|
|
1967
|
+
export function cosine(a, b) {
|
|
1968
|
+
if (!a || !b || a.length !== b.length) return 0 // ← 同维度模型换不掉
|
|
1969
|
+
...
|
|
1970
|
+
}
|
|
1971
|
+
```
|
|
1972
|
+
|
|
1973
|
+
换模型 = 换坐标系。旧坐标在新坐标系里没有意义,但维数一样,代码照常算出数字——
|
|
1974
|
+
不报错、不崩,只表现为「检索结果越来越怪」。这是最难查的一类坏。
|
|
1975
|
+
|
|
1976
|
+
### 解决:记录向量是谁生成的
|
|
1977
|
+
|
|
1978
|
+
新增 `vectors.meta.json`:
|
|
1979
|
+
|
|
1980
|
+
```json
|
|
1981
|
+
{ "sig": "api:BAAI/bge-m3", "dim": 1024, "count": 82, "at": "2026-11-01T03:51:25.260Z" }
|
|
1982
|
+
```
|
|
1983
|
+
|
|
1984
|
+
- `sig` 由插件传入:`api:<embed 阶段解析后的模型名>`;无模型时是 `none`
|
|
1985
|
+
(v0.5.2 之前叫 `local-hash`,那是本地哈希向量的标记,现已不存在,见第 23 节)
|
|
1986
|
+
- 加载分片时比对 `sig`,**不一致就把旧向量整体搬到 `vectors.stale-<ts>/`**,然后向量表归零
|
|
1987
|
+
- 老库没有 meta 时**按兼容处理**(直接沿用 + 补写指纹),不能因为加了个字段就让用户崩
|
|
1988
|
+
- 隔离而不是删除:出问题还能人工核对或整体搬回
|
|
1989
|
+
|
|
1990
|
+
验证:
|
|
1991
|
+
|
|
1992
|
+
```
|
|
1993
|
+
① 同指纹加载: 1 条 ← 正常加载
|
|
1994
|
+
② 换模型后: 0 条 | 剩余分片 0 | 隔离目录 vectors.stale-...
|
|
1995
|
+
保存后 meta: {"sig":"api:BAAI/bge-m3","dim":0,"count":0}
|
|
1996
|
+
③ 新指纹再打开: 0 条 ← 不会反复搬家
|
|
1997
|
+
④ 老库无指纹: 1 条 | 补写 meta ← 向后兼容
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
### 重建工具 `reindex.mjs`
|
|
2001
|
+
|
|
2002
|
+
```bash
|
|
2003
|
+
node reindex.mjs --dry # 体检:几条、什么模型、指纹是啥
|
|
2004
|
+
node reindex.mjs # 真重嵌(默认先备份旧向量)
|
|
2005
|
+
node reindex.mjs --force # 不备份
|
|
2006
|
+
```
|
|
2007
|
+
|
|
2008
|
+
要点:
|
|
2009
|
+
|
|
2010
|
+
- **截断口径必须和运行时一致**(`slice(0, 512)`)。重建用 8192、线上用 512,
|
|
2011
|
+
两批向量就不是同一个分布了,等于白做。
|
|
2012
|
+
- 分片格式一致:base64 的 `Float32Array`,每片 5000 条
|
|
2013
|
+
- 写完 meta,`sig` 与运行时一致 → 重启后不会再触发隔离
|
|
2014
|
+
- 没有 `apiKey` 直接退出并说明原因(没有 embedding 模型,重建无从谈起)
|
|
2015
|
+
|
|
2016
|
+
### 换 embedding 后的旧条目
|
|
2017
|
+
|
|
2018
|
+
重建前隔离掉的旧向量里,条目文本都还在 `store.json`,所以:
|
|
2019
|
+
**跑一次 `reindex.mjs` 就能全部补回来**,条目本身不会丢。
|
|
2020
|
+
不跑的话,旧条目只能被关键词命中,`memory_search` 的语义部分对它们失效。
|
|
2021
|
+
|
|
2022
|
+
---
|
|
2023
|
+
|
|
2024
|
+
## 23. 移除本地哈希向量(2026-11-01,v0.5.2)
|
|
2025
|
+
|
|
2026
|
+
> 用户的判断:`hashEmbed` 没用——它给的是字符分布相似度,和语义无关,
|
|
2027
|
+
> 却因为同样是 1024 维,跟真模型混在一起分不清。宁可没有,也别假装有。
|
|
2028
|
+
|
|
2029
|
+
### 改了什么
|
|
2030
|
+
|
|
2031
|
+
```diff
|
|
2032
|
+
- : { embed: hashEmbed } // 无 key 时的"兜底"
|
|
2033
|
+
+ : {} // 没有模型就什么都不给
|
|
2034
|
+
```
|
|
2035
|
+
|
|
2036
|
+
- `hashEmbed` 函数整个删除
|
|
2037
|
+
- 无 key 时 `embedSignature = 'none'`,存储层进入 **vectorsDisabled** 模式:
|
|
2038
|
+
- **不读**向量文件(读了也没用:查询侧没有 embed,qVec 恒为 null)
|
|
2039
|
+
- **不写**向量文件(现有向量可能是真模型生成,不能被一次空表覆盖)
|
|
2040
|
+
- 也就是说:没模型时向量文件原样躺着,接上模型立刻还能用
|
|
2041
|
+
- 接上模型后指纹从 `none` 变成 `api:xxx` → 旧哈希向量被隔离到 `vectors.stale-<ts>/`
|
|
2042
|
+
|
|
2043
|
+
### 检索行为变化
|
|
2044
|
+
|
|
2045
|
+
`searchOnce` 里 `sem` 恒为 0,只剩关键词 + 热度 + 重要度。有一个必须补的例外:
|
|
2046
|
+
|
|
2047
|
+
```js
|
|
2048
|
+
// 时间窗查询不能被"零命中"筛掉
|
|
2049
|
+
if (win) {
|
|
2050
|
+
if (!noSem && hit === 0 && sem < 0.2) continue // ← 加 noSem 条件
|
|
2051
|
+
}
|
|
2052
|
+
```
|
|
2053
|
+
|
|
2054
|
+
「昨天发生了什么」去掉时间词只剩"发生什么",没有任何一条会含这个词,
|
|
2055
|
+
语义又用不上——老规则会把整段时间的记忆全排掉,明明东西都在库里。
|
|
2056
|
+
时间窗本身就是强约束,这时按热度排就行。
|
|
2057
|
+
|
|
2058
|
+
### 实测
|
|
2059
|
+
|
|
2060
|
+
```
|
|
2061
|
+
① signature=none 加载: 0 条(不读,但文件原样保留)
|
|
2062
|
+
save 后: vectors-part-0000.json, vectors.meta.json 都还在
|
|
2063
|
+
② 接上模型: 0 条 + 旧哈希向量被隔离
|
|
2064
|
+
|
|
2065
|
+
无模型时的检索:
|
|
2066
|
+
关键词 "bge-m3" → 1 条(命中)
|
|
2067
|
+
时间窗 "昨天" → 2 条(靠窗口本身)
|
|
2068
|
+
"拉面" → 1 条
|
|
2069
|
+
"量子计算机" → 0 条(不瞎给,这正是哈希向量做不到的)
|
|
2070
|
+
```
|
|
2071
|
+
|
|
2072
|
+
### 代价
|
|
2073
|
+
|
|
2074
|
+
- 无 key 时 **没有语义检索,也没有摘要**(摘要本来也依赖模型)
|
|
2075
|
+
- 好处:不再有"看起来在搜、其实在按字符重叠排序"的假象
|
|
2076
|
+
|
|
2077
|
+
|
|
2078
|
+
**结论:语义检索从"默认降级"变成"必须显式开启"。** 这是有意的。
|
|
2079
|
+
|
|
2080
|
+
---
|
|
2081
|
+
|
|
2082
|
+
## 24. 模型配置改版 + 融合/摘要跟会话模型(2026-11-01,v0.6.0)
|
|
2083
|
+
|
|
2084
|
+
> 用户两条要求:① 嵌入/重排给"硅基流动免费"和"自己指定"两个选择;
|
|
2085
|
+
> 融合与摘要**默认跟着当前对话用的模型走**(对话用 flash 就 flash),能改、能关思考。
|
|
2086
|
+
> ② 设置页开头每层存量要写清楚多少条、多少字。
|
|
2087
|
+
|
|
2088
|
+
### 能不能拿到当前会话模型?能
|
|
2089
|
+
|
|
2090
|
+
翻 dsh 的包找到两条现成的路子:
|
|
2091
|
+
|
|
2092
|
+
| 服务 | 用法 | 给什么 |
|
|
2093
|
+
|---|---|---|
|
|
2094
|
+
| `ctx.get('agentDefaultModel')` | `.currentSelection()` | `{provider, model, reasoningEffort}` |
|
|
2095
|
+
| `ctx.get('llm')` | `.prepareCall({provider, model, maxTokens}, signal, configure)` | 一次流式调用 |
|
|
2096
|
+
|
|
2097
|
+
官方插件 `@deepseek-ai/dsh-session-title-llm` 就是这么打辅助请求的——照抄它的调用姿势。
|
|
2098
|
+
好处是**不用我们自己的 key**,dsh 自己有鉴权;对话换模型,这边跟着换。
|
|
2099
|
+
|
|
2100
|
+
### 关思考:没有统一开关
|
|
2101
|
+
|
|
2102
|
+
`LlmCallControls` 里没有 `enable_thinking` 之类的东西,只有 `reasoningEffort`
|
|
2103
|
+
(**适配器私有的不透明字符串**,各家不一样)。正确做法是从 `configure` 的第二个参数
|
|
2104
|
+
`modelInfo.reasoning.efforts` 里取**最低档**——`efforts` 是从小到大排的,
|
|
2105
|
+
最低档就是这个模型能选的"最少思考":
|
|
2106
|
+
|
|
2107
|
+
```js
|
|
2108
|
+
llm.prepareCall({ provider, model, maxTokens }, signal, (controls, modelInfo) => {
|
|
2109
|
+
const efforts = modelInfo?.reasoning?.efforts
|
|
2110
|
+
return { ...controls, reasoningEffort: efforts?.[0]?.id ?? controls.reasoningEffort }
|
|
2111
|
+
})
|
|
2112
|
+
```
|
|
2113
|
+
|
|
2114
|
+
流式回来的 chunk 里 `reasoning-delta` 就是思考内容,**只收 `text-delta`**,其余丢掉。
|
|
2115
|
+
|
|
2116
|
+
> ⚠️ 踩不到的坑先记着:`prepareCall` 之后**不能再改** `maxTokens` / `temperature`——
|
|
2117
|
+
> 配置被冻住了,改了会抛 `prepared LLM call config changed before adapter dispatch`。
|
|
2118
|
+
> 要改就在 `configure` 里改 controls。
|
|
2119
|
+
|
|
2120
|
+
### 新的配置形状
|
|
2121
|
+
|
|
2122
|
+
```json
|
|
2123
|
+
{
|
|
2124
|
+
"preset": "siliconflow",
|
|
2125
|
+
"apiKey": "sk-...",
|
|
2126
|
+
"models": {
|
|
2127
|
+
"chat": { "source": "session" },
|
|
2128
|
+
"summary": { "source": "session" }
|
|
2129
|
+
},
|
|
2130
|
+
"noThink": { "chat": true, "summary": false }
|
|
2131
|
+
}
|
|
2132
|
+
```
|
|
2133
|
+
|
|
2134
|
+
- `preset: siliconflow` → 嵌入 `BAAI/bge-m3`、重排 `BAAI/bge-reranker-v2-m3`(默认已改成这俩)
|
|
2135
|
+
- `preset: custom` → 用 `models.embed/rerank` 里填的 `{base, apiKey, model}`
|
|
2136
|
+
- `source: session` → 走会话模型;`api` 或有外部连接参数 → 走 HTTP
|
|
2137
|
+
- 任一阶段调用失败/返回空,安静回落 HTTP,不影响主流程
|
|
2138
|
+
|
|
2139
|
+
### 设置页
|
|
2140
|
+
|
|
2141
|
+
顶部换成一张表:每层 **条数 + 字数 + 这层是什么**,进上下文的层打了「进上下文」标签。
|
|
2142
|
+
|
|
2143
|
+
```
|
|
2144
|
+
层 条数 字数 存的是什么
|
|
2145
|
+
L1 即时 9 735 当前会话原话… [进上下文]
|
|
2146
|
+
L2 巩固 6 480 较重要原话… [进上下文]
|
|
2147
|
+
L3 长期 11 1900 摘要视图… [进上下文]
|
|
2148
|
+
DL1 遗忘中 …
|
|
2149
|
+
DL2 原文库 …
|
|
2150
|
+
DL3 摘要库 44 6000
|
|
2151
|
+
向量 82 - 有向量才能语义检索
|
|
2152
|
+
```
|
|
2153
|
+
|
|
2154
|
+
模型分区:嵌入/重排是二选一卡片;融合/摘要是「跟随当前会话模型 / 自己指定」二选一,
|
|
2155
|
+
选了自己指定才展开模型、地址、Key 三个输入框(`showIf` 控制)。
|
|
2156
|
+
文案按用户要求写死:融合建议便宜小模型,摘要建议比它大一号的。
|
|
2157
|
+
|
|
2158
|
+
### 验证
|
|
2159
|
+
|
|
2160
|
+
```
|
|
2161
|
+
假 dsh(llm + agentDefaultModel 都是 mock):
|
|
2162
|
+
session: deepseek-v4-flash
|
|
2163
|
+
embed HTTP → BAAI/bge-m3
|
|
2164
|
+
rerank HTTP → BAAI/bge-reranker-v2-m3
|
|
2165
|
+
chat 跟随会话模型 → deepseek-v4-flash
|
|
2166
|
+
summary 跟随会话模型 → deepseek-v4-flash
|
|
2167
|
+
configure 取到最低档 minimal;思考内容被丢弃;只返回 "duplicate"
|
|
2168
|
+
|
|
2169
|
+
写 preset=custom + 摘要走 api + noThink.summary=true:
|
|
2170
|
+
applied: ["preset","models.summary.base","models.summary.model","models.summary.source","noThink.summary"]
|
|
2171
|
+
回归 41+20+17 全过
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2174
|
+
---
|
|
2175
|
+
|
|
2176
|
+
## 26. 六文件布局(v0.8.0,覆盖第 13 节的多写者方案)
|
|
2177
|
+
|
|
2178
|
+
> 第 13 节是 v0.7.0 的"每个写者一个分片、里面还是六层"。v0.8.0 起默认走
|
|
2179
|
+
> `memory6.mjs` 的六文件布局;第 13 节只在 `layout: "legacy"` 时仍然成立。
|
|
2180
|
+
|
|
2181
|
+
### 为什么换
|
|
2182
|
+
|
|
2183
|
+
分片方案只做到"数据能互相看见":对方条目标 `__foreign`,`moveTo` / `fuseDL3` /
|
|
2184
|
+
`markSupersede` / `attend` 全跳过——**生命周期不共享**。六文件布局把整理权收给
|
|
2185
|
+
当轮拿到钥匙的一方,其余端只写自己的 temp。
|
|
2186
|
+
|
|
2187
|
+
### 文件
|
|
2188
|
+
|
|
2189
|
+
```
|
|
2190
|
+
memory/
|
|
2191
|
+
L1.json 真 L1(单文件,只有拿钥匙的一方写)
|
|
2192
|
+
L1.tmp/<writer>.jsonl 每端追加自己的当前轮(追完就关)
|
|
2193
|
+
L1.tmp/*.claimed-* 拿钥匙的人 rename 认领过来、正在并的(瞬态)
|
|
2194
|
+
L2.json 缓冲层(原文)
|
|
2195
|
+
L3.json DL3 的排序快照(注入只读它,可从 DL3 重建)
|
|
2196
|
+
DL1.json 流水(原文)
|
|
2197
|
+
DL2.json 摘要引用的原文(只收被引用的,绝不单独进)
|
|
2198
|
+
DL3.json 摘要池(真身)
|
|
2199
|
+
signals/<writer>.jsonl attend / pin / forget 请求
|
|
2200
|
+
vectors/deep-*.json 整理者唯一写;两端只读
|
|
2201
|
+
organize.lock 钥匙(心跳 10 分钟过期可接管)
|
|
2202
|
+
```
|
|
2203
|
+
|
|
2204
|
+
**六个文件** = `L1/L2/L3/DL1/DL2/DL3`;`L1.tmp/` 是 L1 的暂存区,不算第七层。
|
|
2205
|
+
|
|
2206
|
+
### 流程
|
|
2207
|
+
|
|
2208
|
+
```
|
|
2209
|
+
端每次 turn 结束:
|
|
2210
|
+
1. 这一轮的行(user / bot / tools,同一个 turnId)追加到 L1.tmp/<writer>.jsonl
|
|
2211
|
+
2. 尝试拿钥匙(open('wx') 创建 organize.lock)
|
|
2212
|
+
├ 拿不到 → 本轮不整理,输入不受影响
|
|
2213
|
+
└ 拿到 → 合并(只有它做):
|
|
2214
|
+
a. 扫遗留 *.claimed-*;把两边 temp rename 认领 → 按 id 去重并进 L1.json → 删 claimed
|
|
2215
|
+
b. 先消费 signals(attend 可能把 DL1 升回 L2,别被 L2 摘要抢先)
|
|
2216
|
+
c. L1 超 hotMaxChars/Items → 只挑"安静了 quietMs"的轮次,按 turnId 整轮取,
|
|
2217
|
+
直到剩下 ≤ 上限/2 → 摘要(DL3/DL2/L2/DL1)
|
|
2218
|
+
d. L2 超容量 → 同样整轮取旧半 + DL1 全部 → 再摘要;
|
|
2219
|
+
第二次的 `+` 留在 L2(本轮最多 `l2KeepMax` 条,超出进 DL1),**绝不进 DL2**
|
|
2220
|
+
e. 遗忘 DL1、saveDb(**先写 L2/DL1/DL2/DL3,最后写 L1**)、落向量
|
|
2221
|
+
f. L3:内容变了或到 viewRefreshMs 才重写快照
|
|
2222
|
+
g. 删钥匙
|
|
2223
|
+
```
|
|
2224
|
+
|
|
2225
|
+
**触发**:一端结束 + `organizeEveryMs`(默认 5 分钟)定时器;两者都只是"尝试拿钥匙"。
|
|
2226
|
+
**不阻塞输入**:拿不到钥匙就跳过,整理晚一轮没有影响。
|
|
2227
|
+
|
|
2228
|
+
### 关键不变式
|
|
2229
|
+
|
|
2230
|
+
- **DL2 只收被摘要引用的原文**。`cid`(cited)→ DL2;`+` 只回 L2;没人提到的 → DL1。
|
|
2231
|
+
摘要被删后留下的 DL2 孤儿,由 reconcile 移到 DL1。
|
|
2232
|
+
- **按 turnId 整轮取**:问题和回答不会被拆到两批。
|
|
2233
|
+
- **先写目标层,最后写 L1**:崩在中间 L1 还有原文,靠 id 去重,不重复不丢。
|
|
2234
|
+
- **整理卡太久被接管时**,`checkLock()` 发现钥匙不是自己的就放弃写入。
|
|
2235
|
+
- 注入读 `L1.json` + 所有 `L1.tmp/*`,刚结束的一轮下一轮就能看见(不等合并)。
|
|
2236
|
+
|
|
2237
|
+
### 与 memory5 的差异
|
|
2238
|
+
|
|
2239
|
+
| 项 | memory5(legacy) | memory6(split) |
|
|
2240
|
+
|---|---|---|
|
|
2241
|
+
| L1 | 每人一个 `store-<writer>.json`,里面六层 | 真 L1 + 每端 temp |
|
|
2242
|
+
| 整理 | 每端自己跑 `enforceLimits` | 谁先结束谁拿钥匙,只跑一份 |
|
|
2243
|
+
| 跨端 | 对方条目只读,不融合/不 attend | 深层单写者,跨端融合/检索/attend 都成立 |
|
|
2244
|
+
| L1 溢出 | 取最老一半(条数) | 按 turnId 整轮取,剩下 ≤ 一半 |
|
|
2245
|
+
| L1 内容 | 用户输入 + 工具 | 用户 + **assistant 正文(整轮 400 字)** + 工具 |
|
|
2246
|
+
| L3 | 内存视图,30 分钟算一次 | `L3.json` 快照,内容变/到间隔才重写 |
|
|
2247
|
+
| 检索 | 遍历 store.items,向量只读自己的 | 按层读文件,向量共用;detail 顺着 sources 下探 DL2 |
|
|
2248
|
+
| 配置 | `hotMaxChars: 2000` | `hotMaxChars: 3000`,`botCaptureChars: 400`,`quietMs: 10min`,`l2KeepMax: 10` |
|
|
2249
|
+
|
|
2250
|
+
### 验收
|
|
2251
|
+
|
|
2252
|
+
```
|
|
2253
|
+
node test-memory6.mjs 48 / 48
|
|
2254
|
+
node test-m5.mjs 41 / 41(legacy 回归)
|
|
2255
|
+
node test-batch.mjs 20 / 20
|
|
2256
|
+
node test-share.mjs 25 / 25(legacy 分片)
|
|
2257
|
+
真实模型手测:organize → DL3 摘要 + DL2 原文;search detail 命中 rel 0.93,原文标 via
|
|
2258
|
+
```
|
|
2259
|
+
|
|
2260
|
+
### 已知限制
|
|
2261
|
+
|
|
2262
|
+
- **不迁移旧数据**:split 从空开始,旧 `store.json` 原样保留;`layout: "legacy"` 可回退。
|
|
2263
|
+
- **两端必须都用 memory6**:astrbot 还在用 memory5 就不会写 `L1.tmp/astrbot.jsonl`,等于没共享。
|
|
2264
|
+
- **两端 embed 模型必须一致**(默认 `BAAI/bge-m3`);其余阶段可不同。
|
|
2265
|
+
- **同名 writer 会写同一个 temp**:每个程序必须通过插件配置或 `MEMORY_WRITER` 给自己起不同的名字。
|
|
2266
|
+
- 设置页还没加 `layout` / `botCaptureChars` / `quietMs` / `organizeEveryMs` 的表单(可手改 config.json)。
|
|
2267
|
+
|
|
2268
|
+
---
|
|
2269
|
+
|
|
2270
|
+
## 27. 待办与决策(唯一清单)
|
|
2271
|
+
|
|
2272
|
+
> 2026-11-02 起,待办、决策、已知取舍都集中在这里;`CHANGES.md` 已并入本节并删除。
|
|
2273
|
+
> 每完成一项就删掉对应行,不要另开文档。
|
|
2274
|
+
|
|
2275
|
+
### 27.1 待决策
|
|
2276
|
+
|
|
2277
|
+
| # | 事项 | 选项 | 为什么还卡着 |
|
|
2278
|
+
|---|---|---|---|
|
|
2279
|
+
| D1 | 五档分档(重要 / 次重要 / 一般 / 琐事 / 无用) | 见下方讨论稿 | 现在的 citation 三路能跑,改了要先想清楚"留在 L2 的那档会不会让 L2 常满" |
|
|
2280
|
+
| D2 | 是否删掉 `routeMode: 'importance'` 旧路径(memory5) | 删 / 留 | 只在 `layout: "legacy"` 时走到;确认不再回退后可删,两套并存容易读错 |
|
|
2281
|
+
|
|
2282
|
+
**五档讨论稿(未定稿)**
|
|
2283
|
+
|
|
2284
|
+
```
|
|
2285
|
+
L1 分流(判五档)
|
|
2286
|
+
重要 → 原文 DL2 + 摘要 DL3
|
|
2287
|
+
无用 → DL1
|
|
2288
|
+
其余三档 → 原话进 L2(观察)
|
|
2289
|
+
↓ L2 满
|
|
2290
|
+
L2 分流(再判一次)
|
|
2291
|
+
前三档 → DL3
|
|
2292
|
+
某一档 → 留在 L2
|
|
2293
|
+
无用 → DL1
|
|
2294
|
+
```
|
|
2295
|
+
|
|
2296
|
+
三处没定:
|
|
2297
|
+
|
|
2298
|
+
1. 档位顺序:`重要 > 次重要 > 一般 > 琐事 > 无用`,还是 `重要 > 一般 > 琐事 > 次重要 > 无用`
|
|
2299
|
+
2. L2 分流时"留一档"留的是哪一档
|
|
2300
|
+
3. 留在 L2 的那档会让 L2 长期满仓(每次写入都触发整理)→ 收口规则是"留一轮"还是"只留最近 N 条"
|
|
2301
|
+
|
|
2302
|
+
**硬约束**:五档必须和摘要**一次调用出**(`{摘要, 出处编号, 档位}`)。分两次判会出现"判了重要但没摘要"和"摘要覆盖的原文被判成无用"这种对不上的情况。
|
|
2303
|
+
|
|
2304
|
+
### 27.2 还没做
|
|
2305
|
+
|
|
2306
|
+
**性能(有实测依据,数据见 27.7)**
|
|
2307
|
+
|
|
2308
|
+
| # | 问题 | 现状 | 方案 | 什么时候必须做 |
|
|
2309
|
+
|---|---|---|---|---|
|
|
2310
|
+
| ~~P1~~ | ~~检索每次都把目标层整个 JSON 读出来解析~~ | **✅ 2026-11-02 已做**:10 万条 summary 481→50 ms、detail 692→91 ms(见 27.7 四) | 只读层缓存 + 按 id 索引;默认 384 MB,超预算自动退回直读 | — |
|
|
2311
|
+
| P2 | 百万条时解析超时 | 100 万条:summary 4 s / detail 16 s / full 19 s,堆 3.4 GB | 按时间分片(按月或每 5 万条一个文件),检索按时间窗只读相关片 | 深层超过 50 万条 |
|
|
2312
|
+
| P3 | 摘要慢在**输出**不在输入 | 读 6.7 万 token 约 11 s;生成 5 千 token 约 42 s(≈120 token/s) | 给提示词加"最多 N 条"上限(`maxLines` 已在返回里,代码也会按它截断,只是没强调);压缩率从 1/9 降到 1/20 约快 2 倍 | 想缩短整理时间就做 |
|
|
2313
|
+
| P4 | 并发提速不可用 | 硅基流动免费/低档账户并发很紧,4 批并发直接 429(`code 50609`);串行分批反而更慢(60.6 s vs 42 s) | 提高账户配额,或换 provider;代码层面不要指望并发 | 不是代码问题 |
|
|
2314
|
+
|
|
2315
|
+
**功能**
|
|
2316
|
+
|
|
2317
|
+
- **设置页表单**:`layout` / `botCaptureChars` / `quietMs` / `organizeEveryMs` 已进 `config.json` 白名单,表单还没加,只能手改。
|
|
2318
|
+
- **astrbot 侧接入**:它必须写 `L1.tmp/astrbot.jsonl` 和 `signals/astrbot.jsonl` 才会真正共享(格式见第 26 节)。现在只有 dsh 在写。
|
|
2319
|
+
- **旧数据导入**:`store.json` 里的 82 条不迁移;想保留需要一个一次性转换脚本(旧 → 六个文件)。
|
|
2320
|
+
- **向量索引 / HNSW**:仍按线性余弦,见第 10 节。P2 之后才轮到它。
|
|
2321
|
+
|
|
2322
|
+
### 27.3 清理项
|
|
2323
|
+
|
|
2324
|
+
| # | 内容 | 为什么 |
|
|
2325
|
+
|---|---|---|
|
|
2326
|
+
| C1 | `memory5.mjs:78` 那句"L2 是一次性整层取走" | 和 `2215` 的实现直接矛盾(实际取旧半 + DL1 全部) |
|
|
2327
|
+
| C2 | L1 剥离 `heat` / `supersededBy` 等元数据 | 实践影响很小,优先级最低 |
|
|
2328
|
+
|
|
2329
|
+
### 27.4 已知取舍(不是 bug,别再翻)
|
|
2330
|
+
|
|
2331
|
+
- **注入不核对深层**(2026-11-02):`readL1View` 不再全量读 DL1/DL2/DL3,只读 L1 + temps。
|
|
2332
|
+
正常流程下 L1 不含深层 id;只有崩溃后才可能出现交叉,那时会**重复显示一条**(不是丢),
|
|
2333
|
+
下一次整理由 `reconcile` 清掉。换来注入从"每轮 7 个文件"降到 L1 + L2 + L3(+temps)。
|
|
2334
|
+
- **L3 快照的 `l3Dirty` 是进程内的**,跨进程靠 `dl3v`(DL3 的 mtime+size)判断过期;
|
|
2335
|
+
快照过期时 `buildBlock` 现算一份,不写盘(写盘是拿钥匙那一方的职责)。
|
|
2336
|
+
- **DL2 只有一个入口**:被摘要引用。`+` 永远回 L2,绝不进 DL2;孤儿由 `reconcile` 移到 DL1。
|
|
2337
|
+
|
|
2338
|
+
### 27.5 发布状态
|
|
2339
|
+
|
|
2340
|
+
| 项 | 状态 |
|
|
2341
|
+
|---|---|
|
|
2342
|
+
| 本地仓库 | 0.8.0 + 2026-11-02 的修复(未提交到 npm) |
|
|
2343
|
+
| npm | 0.5.1 |
|
|
2344
|
+
| 要用上新版 | 本地 link 已是最新(重启 dsh 生效);发 npm 时 `files` 里要带 `memory6.mjs` |
|
|
2345
|
+
|
|
2346
|
+
### 27.6 真实模型仿真结论(2026-11-02)
|
|
2347
|
+
|
|
2348
|
+
脚本 `sim-real.mjs`(`--dry` 用假模型、不花钱),九组机制各跑一遍,24 项断言全过。
|
|
2349
|
+
|
|
2350
|
+
| 机制 | 实测 |
|
|
2351
|
+
|---|---|
|
|
2352
|
+
| L1 溢出 | 16 条里取走 10 条(按 turnId 整轮),剩 99 字 ≤ 上限/2 |
|
|
2353
|
+
| 摘要路由 | 10 条 → 1 条摘要、引用 2 条进 DL2,其余 8 条进 DL1;DL2 全部有摘要引用 |
|
|
2354
|
+
| L2 溢出 | 旧半 + DL1 全部再整理一轮,DL3 从 1 → 2;同一 id 不会同时出现在两层 |
|
|
2355
|
+
| 融合 | 重复的命令 → `FUSE_UPDATE`(两条都留、记下先后);没触发合并是因为模型判 update 不是 duplicate |
|
|
2356
|
+
| 检索 | summary 命中 DL3(rel 0.41);detail 带回 11 条,其中 5 条标 `via`;无关话题 advice 给"用前自己核一下" |
|
|
2357
|
+
| L3 排序 | 3 条按 importance×heat 降序,85 字 ≤ 800 |
|
|
2358
|
+
| DL3 热度衰减 | 同一条摘要 30 天后 2.00 → 0.40,分数 1.80 → 0.36 |
|
|
2359
|
+
| DL1 遗忘 | 推进 30 天,21 条全部进台账(保持率 < 0.25) |
|
|
2360
|
+
| 注入块 | 459 字,三段顺序 0 < 154 < 307(L3 → L2 → L1) |
|
|
2361
|
+
|
|
2362
|
+
**三个值得知道的结论**:
|
|
2363
|
+
|
|
2364
|
+
1. **L1 → DL3 的门禁很严**:一批 10 条只产出 1 条 ★ 摘要、引用 2 条,其余 8 条直接进 DL1。
|
|
2365
|
+
这是设计(DL3 是稀有长期记忆),但意味着 **DL3 增长比直觉慢得多**,一段时间内 L3 段会很少。
|
|
2366
|
+
2. **判关系模型不稳定**:同样两条摘要,连测两次判 `update`,换一对判 `contradiction`,很少判 `duplicate`。
|
|
2367
|
+
所以"融合去重"的实际效果取决于模型措辞判断,不是阈值问题。提示词已补一条规则,仍需长期观察。
|
|
2368
|
+
3. **`update` 原来没有分支**:memory5 有、memory6 漏了。模型判"进展"时既不记 `updates`
|
|
2369
|
+
也不打审计,表现就是"融合完全没生效"。已补。
|
|
2370
|
+
|
|
2371
|
+
成本:embed 9 次 / 38 条(bge-m3 免费)、summarize 3 次 / 32 条、relation 1 次、rerank 3 次 / 17 条,十来秒。
|
|
2372
|
+
|
|
2373
|
+
### 27.7 大容量基准(2026-11-02,`bench-scale.mjs`)
|
|
2374
|
+
|
|
2375
|
+
**一、一次摘要处理太多条**(真实模型:DeepSeek-V4.1-Flash)
|
|
2376
|
+
|
|
2377
|
+
| 条数 | 字数 | 耗时 | 输入 token |
|
|
2378
|
+
|---|---|---|---|
|
|
2379
|
+
| 50 | 2 571 | 3.5 s | 2 526 |
|
|
2380
|
+
| 200 | 10 792 | 21.8 s | 10 452 |
|
|
2381
|
+
| 500 | 27 412 | 31.0 s | 25 553 |
|
|
2382
|
+
| 1 000 | 55 123 | 68.3 s | 50 608 |
|
|
2383
|
+
| 2 000 | 112 523 | 140.7 s | ~16 万 |
|
|
2384
|
+
|
|
2385
|
+
线性:**约 1.4 s / 千字、1 token / 1.1 字**,2000 条也不失败(模型上下文够)。
|
|
2386
|
+
|
|
2387
|
+
推论:
|
|
2388
|
+
- L1 设 10 万字 → 一次溢出取一半 5 万字 → **约 70 秒**、5 万 token/次。
|
|
2389
|
+
- `maxOrganizeMs` 默认 5 分钟,所以 L1 上限超过 **15 万字**就会撞超时(整理被放弃,摘要白做)。
|
|
2390
|
+
- 整理期间持锁,另一端拿不到钥匙(不影响对话,只是那一轮不整理)。
|
|
2391
|
+
|
|
2392
|
+
**二、深层多大时检索开始慢**(纯本地,不连模型)
|
|
2393
|
+
|
|
2394
|
+
| 每层条数 | DL 文件合计 | 建库 | 注入 | summary | detail | full |
|
|
2395
|
+
|---|---|---|---|---|---|---|
|
|
2396
|
+
| 1 万 | 9.7 MB | 95 ms | 6 ms | 36 ms | 158 ms | 179 ms |
|
|
2397
|
+
| 10 万 | 97 MB | 843 ms | 2 ms | 368 ms | 1.40 s | 1.66 s |
|
|
2398
|
+
| 30 万 | 294 MB | 2.5 s | 14 ms | 1.14 s | 3.99 s | 5.23 s |
|
|
2399
|
+
| 100 万 | 980 MB | 9.0 s | 5 ms | 4.09 s | 15.9 s | 19.2 s |
|
|
2400
|
+
|
|
2401
|
+
带模型(10 万条):summary 1.04 s、detail 1.57 s、full 1.84 s —— 比纯本地多 0.2~0.7 s,
|
|
2402
|
+
也就是说 **大头是读文件解析,不是模型调用**。
|
|
2403
|
+
|
|
2404
|
+
- **注入是恒定的**(5~14 ms):`buildBlock` 只读 L1/L2/L3,不碰深层。
|
|
2405
|
+
- **检索是线性的**:每次 `search` 都把目标层整个 JSON 读出来 `JSON.parse`,10 万条 0.4~1.7 s,
|
|
2406
|
+
百万条 4~19 s,不可用。堆峰值 100 万条约 3.4 GB。
|
|
2407
|
+
|
|
2408
|
+
对应的待办(解析缓存 / 时间分片 / 摘要条数上限 / 并发配额)见 **27.2 的 P1–P4**。
|
|
2409
|
+
|
|
2410
|
+
**三、耗时到底花在哪(2026-11-02 补充,`bench-profile.mjs`)**
|
|
2411
|
+
|
|
2412
|
+
| 输入 | 输入 token | 首 token | 说明 |
|
|
2413
|
+
|---|---|---|---|
|
|
2414
|
+
| 1 万字 | 6 780 | 1.6 s | |
|
|
2415
|
+
| 5 万字 | 33 717 | 4.7 s | |
|
|
2416
|
+
| 10 万字 | 67 391 | 10.9 s | 含排队 |
|
|
2417
|
+
|
|
2418
|
+
| 场景 | 输出 token | 耗时 |
|
|
2419
|
+
|---|---|---|
|
|
2420
|
+
| 短输入、写长 | 3 279 | 28.9 s(≈113 token/s) |
|
|
2421
|
+
| 2 000 条一次摘要 | 5 045 | 42.0 s(≈120 token/s) |
|
|
2422
|
+
| 2 000 条分 4 批串行 | 3 683 | 60.6 s(固定开销叠加,更慢) |
|
|
2423
|
+
|
|
2424
|
+
**单位成本差 50 倍**:读 67k token 约 11 秒(0.16 ms/token,prefill 并行);生成 5k token 约 42 秒(8.3 ms/token,逐 token 输出)。
|
|
2425
|
+
|
|
2426
|
+
所以 2000 条那 140 秒的构成是:**读输入约 11 秒 + 生成 1.5 万 token 约 125 秒**。
|
|
2427
|
+
瓶颈是「摘要条数 × 每条长度」,不是输入字数,也不是入库(`moveItem` / `pushSummary` 是本地数组操作,毫秒级;向量是批量一次)。
|
|
2428
|
+
|
|
2429
|
+
**推论**:
|
|
2430
|
+
|
|
2431
|
+
- 输入 10 万字本身不是问题,问题是一次会产出 200 多条摘要(约 1/9 压缩率)。
|
|
2432
|
+
- 想快只有两条路:**少产出**(限制摘要条数/长度),或**并发**(受账户并发配额限制,免费档基本拿不到)。
|
|
2433
|
+
- 串行分批不会更快:每批都要付一次固定的 prefill + 排队开销。
|
|
2434
|
+
|
|
2435
|
+
**四、只读层缓存(2026-11-02 已实施)**
|
|
2436
|
+
|
|
2437
|
+
`search` / `buildBlock` / `stats` / `list` 读层时走 `readLayerCached`,按 **mtime + size** 失效——
|
|
2438
|
+
和"每次都读"看到的完全一样,只是省掉 `JSON.parse`。整理路径(`loadDb`)**永远直读**,
|
|
2439
|
+
不吃缓存:`digest` 会就地改条目(layer / text / supersededBy),改到缓存对象上就是"内存有、磁盘没有"。
|
|
2440
|
+
|
|
2441
|
+
`detail` 的 `sources` 下探也改成按 id 查缓存索引(`cachedIndex`),不再每次把 DL2/DL1 整层塞进 Map。
|
|
2442
|
+
|
|
2443
|
+
| 每层条数 | summary | detail | full |
|
|
2444
|
+
|---|---|---|---|
|
|
2445
|
+
| 1 万 | 36 → **12 ms** | 74 → **13 ms** | 26 → 16 ms |
|
|
2446
|
+
| 10 万 | 481 → **50 ms** | 692 → **91 ms** | 125 → 127 ms |
|
|
2447
|
+
| 30 万 | 1.20 s(超预算,不缓存) | 3.78 s | 4.37 s |
|
|
2448
|
+
|
|
2449
|
+
**防内存不足**(三条闸门,触发任一条就退回直读,只影响速度不影响正确性):
|
|
2450
|
+
|
|
2451
|
+
1. 单文件 > `cacheMaxFileBytes`(64 MB)→ 不缓存
|
|
2452
|
+
2. 每层预算 = `cacheMaxTotalBytes × cacheLayerShare`(DL3 0.4 / DL2 0.3 / DL1 0.3);
|
|
2453
|
+
超了**不缓存这一层,也不去挤别的层**——先到先得会让三层互相淘汰,实测比不缓存还慢(953 ms vs 627 ms)
|
|
2454
|
+
3. 堆已用 > `cacheHeapLimitBytes`(1.5 GB)→ 清空缓存,本轮不再写
|
|
2455
|
+
|
|
2456
|
+
默认 `cacheMaxTotalBytes = 384 MB`,够 10 万条三层(约 290 MB 对象)。
|
|
2457
|
+
30 万条以上缓存救不了,必须靠 P2(按时间分片)。
|
|
2458
|
+
|
|
2459
|
+
**语义没变**:缓存是进程私有的,两端各存各的;对方一落盘 mtime 必变,下一次读就重新解析,
|
|
2460
|
+
所以不会出现"两端看到不同 DL3"或"摘要入库后读到旧缓存"。
|