pi-think-anchor 1.0.0 → 1.0.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/README.md +4 -0
- package/index.ts +38 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
监测 thinking 的「专注指纹」,波段漂移时分级拉回,避免长时间发散。
|
|
4
4
|
|
|
5
|
+
## 来源与借鉴
|
|
6
|
+
|
|
7
|
+
蒸馏自「dsh-anchored-monitor」0.3.1(DSH 生态):https://dsh-plugin.org/zh/plugins/aik358/dsh-anchored-monitor —— 机制思想来源,非代码移植(回合级而非流内实时、无独立进程)。判据 v2(证据门槛 + 发散占比)为本机 6151 条日志校准后的本地改进。
|
|
8
|
+
|
|
5
9
|
## 功能
|
|
6
10
|
|
|
7
11
|
- 对每段 thinking 计算专注/发散词频比率,滑动窗口聚合判定波段。
|
package/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ interface TAConfig {
|
|
|
30
30
|
lexicon: { positive: string[]; negative: string[]; neutral: string[] };
|
|
31
31
|
windowSize: number;
|
|
32
32
|
bands: { spec: number; react: number };
|
|
33
|
+
/** v2 主判据:证据门槛 + 发散词占比(与长度无关) */
|
|
34
|
+
score: { minHits: number; specRate: number; reactRate: number };
|
|
33
35
|
weights: { beta: number; eps: number };
|
|
34
36
|
intervention: { l1CooldownMin: number; l2CooldownMin: number; maxL2: number };
|
|
35
37
|
}
|
|
@@ -39,10 +41,15 @@ const DEF_CFG: TAConfig = {
|
|
|
39
41
|
lexicon: { positive: DEF_POS, negative: DEF_NEG, neutral: DEF_NEU },
|
|
40
42
|
windowSize: 5,
|
|
41
43
|
bands: { spec: 1.2, react: 0.5 },
|
|
44
|
+
score: { minHits: 3, specRate: 0.25, reactRate: 0.5 },
|
|
42
45
|
weights: { beta: 0.4, eps: 0.5 },
|
|
43
46
|
intervention: { l1CooldownMin: 10, l2CooldownMin: 15, maxL2: 2 },
|
|
44
47
|
};
|
|
45
48
|
|
|
49
|
+
// 当前轮诊断量(status 显示用;比值/命中量随轮更新)
|
|
50
|
+
let lastHits = 0;
|
|
51
|
+
let lastNegRate = 0;
|
|
52
|
+
|
|
46
53
|
function loadCfg(): TAConfig {
|
|
47
54
|
try {
|
|
48
55
|
if (!existsSync(CONF)) return DEF_CFG;
|
|
@@ -95,6 +102,19 @@ export function bandOf(ratio: number, bands: { spec: number; react: number } = D
|
|
|
95
102
|
return "react";
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
/**
|
|
106
|
+
* v2 判据:先过证据门槛,再看发散词占比 negRate = neg/(pos+neg+neu)。
|
|
107
|
+
* 数据依据(本机 6151 条实测):react 行 thinking 长度中位 1998、词命中中位 2,43% 的 neg<=1
|
|
108
|
+
* —— 旧 ratio(pos/(neg+eps))把「长而散」误判成「反复试探」:571 次 react 里 326 次证据不足。
|
|
109
|
+
* negRate 分离度:spec 中位 0.14 / mixed 0.50 / react 1.00。
|
|
110
|
+
*/
|
|
111
|
+
export function classify(hits: number, negRate: number, c: { minHits: number; specRate: number; reactRate: number }): TABand {
|
|
112
|
+
if (hits < c.minHits) return "none";
|
|
113
|
+
if (negRate >= c.reactRate) return "react";
|
|
114
|
+
if (negRate >= c.specRate) return "mixed";
|
|
115
|
+
return "spec";
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
/** 从 assistant 消息 content 提取 thinking 全文(多形态兼容) */
|
|
99
119
|
export function extractThinking(content: unknown): string {
|
|
100
120
|
try {
|
|
@@ -115,12 +135,14 @@ export function extractThinking(content: unknown): string {
|
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
export type TABand = "spec" | "mixed" | "react";
|
|
138
|
+
export type TABand = "spec" | "mixed" | "react" | "none";
|
|
119
139
|
export type TAPhase = "healthy" | "warning" | "critical";
|
|
120
140
|
export interface TADecision { level: "none" | "L1" | "L2"; phase: TAPhase; reason: string }
|
|
121
141
|
|
|
122
142
|
/** 分级干预决策状态机(纯函数) */
|
|
123
143
|
export function decideNext(phase: TAPhase, band: TABand, l2Tried: number, maxL2: number): TADecision {
|
|
144
|
+
// v2:证据不足的轮次不参与决策(否则 critical 下的 none 会被当成 react 去升 L2)
|
|
145
|
+
if (band === "none") return { level: "none", phase, reason: "证据不足(不评分)" };
|
|
124
146
|
if (phase === "healthy") {
|
|
125
147
|
if (band === "react") return { level: "L1", phase: "warning", reason: "首次跌入 react 波段" };
|
|
126
148
|
if (band === "mixed") return { level: "none", phase: "warning", reason: "进入过渡带观察" };
|
|
@@ -212,20 +234,24 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
212
234
|
const fp = fingerprint(thinking, st.cfg.lexicon);
|
|
213
235
|
if (fp.pos + fp.neg + fp.neu === 0) {
|
|
214
236
|
st.turns++;
|
|
215
|
-
appendLog({ ts: new Date().toISOString(), turn: st.turns, len: thinking.length, pos: 0, neg: 0, neu: 0,
|
|
237
|
+
appendLog({ ts: new Date().toISOString(), turn: st.turns, v: 2, len: thinking.length, pos: 0, neg: 0, neu: 0, ratio: null, hits: 0, negRate: null, band: "none", phase: st.phase, want: "none", acted: "none" });
|
|
216
238
|
return;
|
|
217
239
|
}
|
|
218
240
|
st.window.push(fp);
|
|
219
241
|
if (st.window.length > st.cfg.windowSize) st.window.shift();
|
|
220
242
|
const agg = st.window.reduce((a, b) => ({ pos: a.pos + b.pos, neg: a.neg + b.neg, neu: a.neu + b.neu }), { pos: 0, neg: 0, neu: 0 });
|
|
243
|
+
const hits = agg.pos + agg.neg + agg.neu;
|
|
244
|
+
const negRate = hits > 0 ? agg.neg / hits : 0;
|
|
245
|
+
lastHits = hits;
|
|
246
|
+
lastNegRate = negRate;
|
|
221
247
|
const ratio = ratioOf(agg.pos, agg.neg, agg.neu, st.cfg.weights);
|
|
222
|
-
const band =
|
|
248
|
+
const band = classify(hits, negRate, st.cfg.score);
|
|
223
249
|
const prevPhase = st.phase;
|
|
224
250
|
const d = decideNext(st.phase, band, st.l2Tried, st.cfg.intervention.maxL2);
|
|
225
251
|
st.turns++;
|
|
226
252
|
st.lastRatio = ratio;
|
|
227
253
|
st.lastBand = band;
|
|
228
|
-
st.bandHist[band]++;
|
|
254
|
+
if (band !== "none") st.bandHist[band]++; // none = 证据不足,不计波段(否则 NaN)
|
|
229
255
|
let acted = "none";
|
|
230
256
|
const now = Date.now();
|
|
231
257
|
if (d.level === "none") {
|
|
@@ -234,7 +260,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
234
260
|
st.lastL1 = now;
|
|
235
261
|
st.l1Count++;
|
|
236
262
|
(pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
|
|
237
|
-
{ customType: "think-anchor", content: L1_TEXT +
|
|
263
|
+
{ customType: "think-anchor", content: L1_TEXT + `(发散占比 ${negRate.toFixed(2)} · 命中 ${hits})`, display: false },
|
|
238
264
|
{ deliverAs: "nextTurn" },
|
|
239
265
|
);
|
|
240
266
|
acted = "L1";
|
|
@@ -244,26 +270,27 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
244
270
|
st.l2Tried++;
|
|
245
271
|
st.l2Count++;
|
|
246
272
|
(pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
|
|
247
|
-
{ customType: "think-anchor", content: L2_TEXT +
|
|
273
|
+
{ customType: "think-anchor", content: L2_TEXT + `(发散占比 ${negRate.toFixed(2)} · 命中 ${hits},L2 ${st.l2Tried}/${st.cfg.intervention.maxL2})`, display: false },
|
|
248
274
|
{ deliverAs: "nextTurn" },
|
|
249
275
|
);
|
|
250
276
|
acted = "L2";
|
|
251
277
|
st.phase = d.phase;
|
|
252
278
|
} else {
|
|
253
|
-
//
|
|
279
|
+
// 被冷却/上限挡下:记 blocked,避免与「无需动作」混淆(实测 583 次 want=L2 只有 55 次真发)
|
|
254
280
|
st.phase = prevPhase;
|
|
281
|
+
if (d.level !== "none") acted = "blocked-" + d.level;
|
|
255
282
|
}
|
|
256
283
|
if (band === "spec" && st.phase === "healthy") st.l2Tried = 0; // 恢复后重置 L2 计数
|
|
257
284
|
appendLog({
|
|
258
285
|
ts: new Date().toISOString(),
|
|
259
286
|
turn: st.turns,
|
|
287
|
+
v: 2,
|
|
260
288
|
len: thinking.length,
|
|
261
289
|
pos: fp.pos,
|
|
262
|
-
wpos: agg.pos,
|
|
263
|
-
wneg: agg.neg,
|
|
264
|
-
wneu: agg.neu,
|
|
265
290
|
neg: fp.neg,
|
|
266
291
|
neu: fp.neu,
|
|
292
|
+
hits,
|
|
293
|
+
negRate: Math.round(negRate * 100) / 100,
|
|
267
294
|
ratio: Math.round(ratio * 100) / 100,
|
|
268
295
|
band,
|
|
269
296
|
phase: st.phase,
|
|
@@ -301,7 +328,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
301
328
|
" · 消息 " + st.turns +
|
|
302
329
|
" · 波段 spec/mixed/react=" + st.bandHist.spec + "/" + st.bandHist.mixed + "/" + st.bandHist.react +
|
|
303
330
|
" · 干预 L1×" + st.l1Count + " L2×" + st.l2Count +
|
|
304
|
-
" ·
|
|
331
|
+
" · 上轮 命中" + lastHits + " 发散占比" + (Math.round(lastNegRate * 100) / 100) + "(" + st.lastBand + ")";
|
|
305
332
|
if (ctx.hasUI) ctx.ui.notify(line, "info");
|
|
306
333
|
},
|
|
307
334
|
});
|