@raolin2025/claude-code-node 2.8.12 → 2.8.14

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 CHANGED
@@ -68,6 +68,7 @@ cc-node --resume session-1747000000000-abc123
68
68
  | `--verbose` | `-v` | 详细输出 | `false` |
69
69
  | `--no-stream` | | 禁用流式响应 | `false` |
70
70
  | `--max-messages` | | 消息条数上限,超过则折叠早期历史为摘要(解决本地小模型"条数过多变傻") | `0`(关闭) |
71
+ | `--small-model` | | 小模型适配模式(强制工具调用 + 敷衍重试 + 意图引导 + 工具精简) | `false` |
71
72
  | `--stdio` | | **JSON-RPC 服务器模式**(供桥接层/外部客户端接入,见下) | |
72
73
  | `--help` | `-h` | 显示帮助 | |
73
74
 
@@ -176,6 +177,29 @@ cc-node 会自动感知当前所用模型的**上下文窗口长度**,并在
176
177
 
177
178
  > 切换模型(`/model`)后会自动重新探测窗口。`/budget` 也会显示当前窗口与 80% 触发阈值。
178
179
 
180
+ ### 🤖 小模型适配模式(--small-model)
181
+
182
+ > 专为 **本地小模型**(如 27B Q3 量化)设计,让编程工具在弱模型下也能可靠工作。
183
+ > 默认关闭,通过 `--small-model` 或 `config.smallModel=true` 开启。
184
+
185
+ 小模型的核心弱点是"规划 + 工具调用 + 自我纠错"不稳定——常只回 `Solved by sharing best practices.`
186
+ 这类空话、不调工具,或多轮往返后"丢"了目标。该模式把智能从"模型端"转移到"框架端":
187
+
188
+ | 层 | 机制 | 作用 |
189
+ |----|------|------|
190
+ | **A · 兜底** | 强化 system prompt | 明确告诉模型"必须调用工具完成任务,不能只回文字" |
191
+ | **A · 兜底** | 敷衍输出检测 + 重试 | 检测到空话/太短/无工具调用时,追加强引导重试一次 |
192
+ | **A · 兜底** | 工具数量精简 | 按用户指令意图只暴露核心工具子集,降低选择负担 |
193
+ | **B · 替代** | 意图识别 + 引导 | 用规则把"写文档/找文件/跑命令/改代码"映射到明确工具,注入任务引导 |
194
+
195
+ ```bash
196
+ cc-node --api-base http://127.0.0.1:18080/v1 --model ./local-model.gguf \
197
+ --with-notify --small-model --max-messages 80
198
+ ```
199
+
200
+ > **配合 `--max-messages N`**:两者互补——`--max-messages` 解决"条数过多",
201
+ > `--small-model` 解决"模型不会自主调工具"。小模型场景建议一起开启。
202
+
179
203
  ---
180
204
 
181
205
  ## 🛠️ 内置工具(10 个)
@@ -341,6 +365,7 @@ cc-node --api-base http://localhost:11434/v1
341
365
  "maxTurns": 100,
342
366
  "maxBudgetTokens": 128000,
343
367
  "maxMessages": 80,
368
+ "smallModel": true,
344
369
  "permissionMode": "ask",
345
370
  "tools": {
346
371
  "bash": { "timeout": 120 },
@@ -360,6 +385,10 @@ cc-node --api-base http://localhost:11434/v1
360
385
  > **`maxMessages`**:消息条数上限(可选,默认关闭/`0`)。当上下文消息条数超过该值时,
361
386
  > 自动折叠早期历史为摘要(保留 Main goal + 最近 4 轮完整对话),解决本地小模型
362
387
  > "条数过多、token 不高却变傻"的问题。等价于 `--max-messages N`。
388
+ >
389
+ > **`smallModel`**:小模型适配模式(可选,默认关闭/`false`)。开启后启用强制工具调用
390
+ > 引导、敷衍输出检测重试、意图识别 + 工具精简,让弱模型(如 27B Q3 量化)也能可靠
391
+ > 完成编程任务。等价于 `--small-model`。
363
392
 
364
393
  [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
365
394
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "2.8.12",
3
+ "version": "2.8.14",
4
4
  "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming, rich media upload, multi-account management",
5
5
  "type": "module",
6
6
  "main": "src/core/index.js",
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 小模型适配层 (small-model.js) 测试
3
+ *
4
+ * 验证:
5
+ * - 敷衍输出检测(isFillerResponse):空话/太短/命中占位句识别
6
+ * - 意图识别(detectIntent):写文件/找文件/跑命令/改代码等映射
7
+ * - 意图引导(buildIntentGuidance):生成注入的引导文本
8
+ * - 工具精简(selectRelevantTools):按意图筛选核心工具
9
+ * - system prompt 强化(buildSmallModelSystemPrompt)
10
+ */
11
+ import { test } from 'node:test'
12
+ import assert from 'node:assert/strict'
13
+ import {
14
+ isFillerResponse,
15
+ detectIntent,
16
+ buildIntentGuidance,
17
+ selectRelevantTools,
18
+ buildSmallModelSystemPrompt,
19
+ SMALL_MODEL_SYSTEM_PROMPT,
20
+ isSmallModelEnabled,
21
+ } from '../core/small-model.js'
22
+
23
+ // ---- 敷衍输出检测 ----
24
+ test('敷衍检测:命中空话占位句', () => {
25
+ assert.equal(isFillerResponse({ content: 'Solved by sharing best practices.', toolCalls: [] }), true)
26
+ assert.equal(isFillerResponse({ content: 'Done. This is the kind of mistake that costs real money.', toolCalls: [] }), true)
27
+ assert.equal(isFillerResponse({ content: 'Let me think about this.', toolCalls: [] }), true)
28
+ })
29
+
30
+ test('敷衍检测:空回复/太短', () => {
31
+ assert.equal(isFillerResponse({ content: '', toolCalls: [] }), true)
32
+ assert.equal(isFillerResponse({ content: ' ', toolCalls: [] }), true)
33
+ assert.equal(isFillerResponse({ content: 'ok', toolCalls: [] }), true)
34
+ assert.equal(isFillerResponse({ content: '好的', toolCalls: [] }), true)
35
+ })
36
+
37
+ test('敷衍检测:有工具调用不算敷衍', () => {
38
+ assert.equal(isFillerResponse({ content: 'ok', toolCalls: [{ id: '1' }] }), false)
39
+ })
40
+
41
+ test('敷衍检测:正常实质回答不算敷衍', () => {
42
+ const content = '我已经分析了项目结构,发现3个主要模块需要重构,分别是订单、行情和风控模块,建议先处理订单模块。'
43
+ assert.equal(isFillerResponse({ content, toolCalls: [] }), false)
44
+ })
45
+
46
+ // ---- 意图识别 ----
47
+ test('意图识别:写文档任务', () => {
48
+ const r = detectIntent('做好开发计划文档')
49
+ assert.ok(r, '应识别到意图')
50
+ assert.ok(r.toolHint.includes('Write'), '应提示用 Write 工具')
51
+ const r2 = detectIntent('把你的计划写入文档')
52
+ assert.ok(r2 && r2.toolHint.includes('Write'))
53
+ })
54
+
55
+ test('意图识别:阅读文档是读取任务(不误判为写)', () => {
56
+ const r = detectIntent('阅读DEVELOPMENT_PLAN.md文档')
57
+ assert.ok(r, '应识别到意图')
58
+ assert.ok(r.toolHint.includes('Read'), '应提示用 Read')
59
+ assert.ok(!r.toolHint.includes('Write'), '读取任务不应提示 Write')
60
+ })
61
+
62
+ test('意图识别:找文件任务', () => {
63
+ const r = detectIntent('查找src目录下的文件')
64
+ assert.ok(r && r.toolHint.includes('Glob'))
65
+ assert.ok(r.toolHint.includes('Grep'))
66
+ })
67
+
68
+ test('意图识别:跑命令任务', () => {
69
+ const r = detectIntent('运行一下测试')
70
+ assert.ok(r && r.toolHint.includes('Bash'))
71
+ })
72
+
73
+ test('意图识别:改代码任务', () => {
74
+ const r = detectIntent('修复这个bug')
75
+ assert.ok(r && r.toolHint.includes('Edit'))
76
+ })
77
+
78
+ test('意图识别:联网搜索任务', () => {
79
+ const r = detectIntent('搜索一下最新的量化交易资料')
80
+ assert.ok(r && r.toolHint.includes('WebSearch'))
81
+ })
82
+
83
+ test('意图识别:无匹配返回 null', () => {
84
+ assert.equal(detectIntent('你好'), null)
85
+ assert.equal(detectIntent(''), null)
86
+ })
87
+
88
+ // ---- 意图引导 ----
89
+ test('意图引导:生成注入文本', () => {
90
+ const g = buildIntentGuidance('做好开发计划文档', { enable: true })
91
+ assert.ok(g, '应生成引导')
92
+ assert.ok(g.includes('Write'), '引导应包含工具建议')
93
+ assert.ok(g.includes('任务引导'), '应有引导标记')
94
+ })
95
+
96
+ test('意图引导:未启用或未匹配返回 null', () => {
97
+ assert.equal(buildIntentGuidance('做好开发计划文档', { enable: false }), null)
98
+ assert.equal(buildIntentGuidance('你好', { enable: true }), null)
99
+ })
100
+
101
+ // ---- 工具精简 ----
102
+ const fakeTools = [
103
+ { name: 'Bash', description: 'd' },
104
+ { name: 'Read', description: 'd' },
105
+ { name: 'Edit', description: 'd' },
106
+ { name: 'Write', description: 'd' },
107
+ { name: 'Glob', description: 'd' },
108
+ { name: 'Grep', description: 'd' },
109
+ { name: 'WebSearch', description: 'd' },
110
+ { name: 'WebFetch', description: 'd' },
111
+ { name: 'GitTool', description: 'd' },
112
+ { name: 'NpmPublish', description: 'd' },
113
+ ]
114
+
115
+ test('工具精简:按意图保留相关工具', () => {
116
+ const reduced = selectRelevantTools(fakeTools, '做好开发计划文档', { enable: true })
117
+ assert.ok(reduced.length < fakeTools.length, '应精简工具数量')
118
+ assert.ok(reduced.some(t => t.name === 'Write'), '应保留 Write')
119
+ // 不相关的如 NpmPublish 不应保留
120
+ assert.ok(!reduced.some(t => t.name === 'NpmPublish'), '不应保留无关工具')
121
+ })
122
+
123
+ test('工具精简:未启用返回全部', () => {
124
+ assert.equal(selectRelevantTools(fakeTools, 'xx', { enable: false }), fakeTools)
125
+ })
126
+
127
+ test('工具精简:无明确意图时返回核心文件工具', () => {
128
+ const reduced = selectRelevantTools(fakeTools, '你好', { enable: true })
129
+ assert.ok(reduced.length <= 6, '应只保留核心工具')
130
+ assert.ok(reduced.some(t => t.name === 'Bash'))
131
+ })
132
+
133
+ // ---- system prompt 强化 ----
134
+ test('system prompt 强化:追加工具使用铁律', () => {
135
+ const base = 'You are cc-node.'
136
+ const enhanced = buildSmallModelSystemPrompt(base)
137
+ assert.ok(enhanced.includes(base), '应保留基础 prompt')
138
+ assert.ok(enhanced.includes('工具调用 Agent'), '应包含工具调用强化引导')
139
+ assert.ok(enhanced.includes(SMALL_MODEL_SYSTEM_PROMPT), '应追加强化 prompt')
140
+ })
141
+
142
+ // ---- 开关判断 ----
143
+ test('小模型开关判断', () => {
144
+ assert.equal(isSmallModelEnabled({ smallModel: true }), true)
145
+ assert.equal(isSmallModelEnabled({ smallModel: false }), false)
146
+ assert.equal(isSmallModelEnabled(undefined), false)
147
+ })
package/src/core/cli.js CHANGED
@@ -331,6 +331,7 @@ function parseArgs(argv) {
331
331
  resume: null,
332
332
  noStream: false,
333
333
  maxMessages: 0,
334
+ smallModel: false,
334
335
  }
335
336
 
336
337
  let i = 2
@@ -347,6 +348,7 @@ function parseArgs(argv) {
347
348
  case '--verbose': case '-v': args.verbose = true; break
348
349
  case '--no-stream': args.noStream = true; break
349
350
  case '--max-messages': args.maxMessages = parseInt(argv[++i], 10); break
351
+ case '--small-model': args.smallModel = true; break
350
352
  case '--stdio': args.stdio = true; break
351
353
  case '--with-notify': args.withNotify = true; break
352
354
  case '--version':
@@ -368,6 +370,7 @@ Options:
368
370
  -v, --verbose Verbose mode
369
371
  --no-stream Disable streaming
370
372
  --max-messages N Fold history when message count exceeds N (default: 0 = off)
373
+ --small-model Enable small-model adaptation (tool-call enforcement, filler retry, intent guidance)
371
374
  --with-notify Start built-in channel listener (Telegram)
372
375
  (replaces cc-notify daemon — no external script needed)
373
376
  -h, --help Show this help
@@ -530,6 +533,8 @@ const systemPrompt = cliArgs.systemPrompt || DEFAULT_SYSTEM_PROMPT
530
533
  configStore: config,
531
534
  // 消息条数上限(折叠早期历史,解决本地小模型"条数过多变傻");0 = 关闭
532
535
  maxMessages: cliArgs.maxMessages || config.get('maxMessages') || 0,
536
+ // 小模型适配模式(强制工具调用 + 敷衍重试 + 意图引导 + 工具精简)
537
+ smallModel: cliArgs.smallModel || config.get('smallModel') || false,
533
538
  })
534
539
  const engine = new QueryEngine(engineConfig)
535
540
 
@@ -15,6 +15,14 @@ import crypto from 'crypto'
15
15
  import { UserMessage, AssistantMessage, ToolCall, ToolResult, SessionState } from '../types/index.js'
16
16
  import { parseStream, parseNonStreamResponse } from './streaming.js'
17
17
  import { compactMessages, trimToWindow, foldHistoryByCount, trimToolResults } from './compact.js'
18
+ import {
19
+ isSmallModelEnabled,
20
+ buildSmallModelSystemPrompt,
21
+ isFillerResponse,
22
+ buildIntentGuidance,
23
+ selectRelevantTools,
24
+ RETRY_GUIDANCE,
25
+ } from './small-model.js'
18
26
  import { CostTracker } from './cost-tracker.js'
19
27
  import { EnhancedPermissionChecker } from '../security/enhanced-permission.js'
20
28
  import { isLocalLlmServer, buildAuthHeaders } from '../utils/index.js'
@@ -39,6 +47,12 @@ export class QueryEngineConfig {
39
47
  this.maxMessages = options.maxMessages || 0
40
48
  // 发送前常驻工具结果截断长度(字符),防止超长工具结果堆积(0 表示不截断)
41
49
  this.maxToolResultChars = options.maxToolResultChars || 6000
50
+ // 小模型适配模式(让弱模型可靠工作):
51
+ // - 强化 system prompt(强制工具调用)
52
+ // - 敷衍输出检测 + 重试
53
+ // - 工具数量精简 + 意图引导
54
+ // 默认关闭,通过 config.smallModel=true 或 --small-model 开启
55
+ this.smallModel = options.smallModel || false
42
56
  this.permissionMode = options.permissionMode || 'ask'
43
57
  this.verbose = options.verbose || false
44
58
  // API 配置 — 通用 OpenAI 兼容协议
@@ -74,6 +88,8 @@ export class QueryEngine {
74
88
  this.tokenBudget = this.config.tokenBudget || null
75
89
  // 最近一次 processMessage 是否已通过流式回调/直写把正文输出(供调用方避免重复打印 response)
76
90
  this.lastStreamed = false
91
+ // 当前正在处理的用户输入(小模型模式用于按意图精简工具集)
92
+ this.currentUserInput = ''
77
93
  }
78
94
 
79
95
  /**
@@ -89,6 +105,7 @@ export class QueryEngine {
89
105
  this.state.turnCount++
90
106
  this.lastStreamed = false // 本轮是否已流式输出正文
91
107
  this.abortController = new AbortController()
108
+ this.currentUserInput = userInput // 记录当前用户指令(小模型模式按意图精简工具)
92
109
  const userMsg = new UserMessage(userInput, images)
93
110
  this.state.messages.push(userMsg)
94
111
 
@@ -200,11 +217,40 @@ export class QueryEngine {
200
217
  */
201
218
  async _runToolLoop(userMessage) {
202
219
  let finalResponse = ''
220
+ const smallModel = isSmallModelEnabled(this.config)
221
+ // 小模型模式:首轮注入意图引导(层 B),帮助模型锁定该用哪些工具
222
+ let intentGuided = false
223
+ // 敷衍重试次数(层 A):模型没调工具只回空话时,追加强引导重试
224
+ let fillerRetries = 0
225
+ const MAX_FILLER_RETRIES = 1
203
226
 
204
227
  for (let turn = 0; turn < this.config.maxTurns; turn++) {
205
228
  // 发送前硬校验:工具结果可能已使上下文超窗,确保 ≤ 窗口(摘要优先 + 滑动窗口裁剪兜底)
206
229
  this._ensureFitWindow()
207
230
 
231
+ // 小模型模式:首轮若识别到明确意图,把引导【追加到首条 system 消息】。
232
+ // 注意:不能 push 一条新的 system 消息到中间——llama.cpp/Jinja 严格要求
233
+ // system 消息必须在对话开头,中间插 system 会报 "System message must be at the beginning" (500)。
234
+ if (smallModel && !intentGuided && this.state.messages.length > 0) {
235
+ const guidance = buildIntentGuidance(userMessage.content, { enable: true })
236
+ if (guidance) {
237
+ // 找到第一条 system 消息,把引导拼接进去(保持 system 全在开头)
238
+ const sysIdx = this.state.messages.findIndex(m => m.role === 'system')
239
+ if (sysIdx !== -1) {
240
+ const sysMsg = this.state.messages[sysIdx]
241
+ this.state.messages[sysIdx] = {
242
+ ...sysMsg,
243
+ content: (typeof sysMsg.content === 'string' ? sysMsg.content : '') + '\n\n' + guidance,
244
+ }
245
+ } else {
246
+ // 没有 system 消息 → 作为 user 引导插入(跟随在最新 user 之后作为新 user)
247
+ this.state.messages.push({ role: 'user', content: guidance })
248
+ }
249
+ if (this.config.verbose) console.error('[small-model] 已注入意图引导(并入首条 system):' + guidance.slice(0, 60) + '...')
250
+ }
251
+ intentGuided = true
252
+ }
253
+
208
254
  const requestMessages = this._buildRequest(this.state.messages)
209
255
  const response = await this._callLLM(requestMessages, this.state.messages)
210
256
 
@@ -212,8 +258,20 @@ export class QueryEngine {
212
258
  throw new Error('操作已取消')
213
259
  }
214
260
 
215
- // 没有工具调用 → 最终回复
261
+ // 没有工具调用 → 潜在最终回复
216
262
  if (!response.toolCalls || response.toolCalls.length === 0) {
263
+ // 小模型模式:检测敷衍输出(空话/太短/命中占位句),追加强引导重试
264
+ if (smallModel && fillerRetries < MAX_FILLER_RETRIES && isFillerResponse(response)) {
265
+ fillerRetries++
266
+ if (this.config.verbose) {
267
+ console.error(`[small-model] 检测到敷衍输出(第 ${fillerRetries} 次),追加强引导重试`)
268
+ }
269
+ // 把模型的空话 + 强制工具调用提醒注入上下文,再让模型重新决策
270
+ this.state.messages.push(new AssistantMessage(response.content, [], response.reasoningContent))
271
+ this.state.messages.push({ role: 'user', content: RETRY_GUIDANCE })
272
+ continue // 继续循环,重新请求模型
273
+ }
274
+
217
275
  finalResponse = response.content
218
276
  this.state.messages.push(new AssistantMessage(response.content, [], response.reasoningContent))
219
277
  break
@@ -257,9 +315,12 @@ export class QueryEngine {
257
315
  _buildRequest(messages) {
258
316
  const request = []
259
317
 
260
- // 系统提示
318
+ // 系统提示(小模型模式:追加强制工具调用引导)
261
319
  if (this.config.systemPrompt) {
262
- request.push({ role: 'system', content: this.config.systemPrompt })
320
+ const sysContent = isSmallModelEnabled(this.config)
321
+ ? buildSmallModelSystemPrompt(this.config.systemPrompt)
322
+ : this.config.systemPrompt
323
+ request.push({ role: 'system', content: sysContent })
263
324
  }
264
325
 
265
326
  // 历史消息 — 转换为 OpenAI 兼容格式
@@ -390,7 +451,16 @@ export class QueryEngine {
390
451
  }
391
452
 
392
453
  // 构建工具定义
393
- const tools = this.config.tools.map(t => ({
454
+ // 小模型模式:按用户指令意图精简工具集,减少模型的选择负担(层 A)
455
+ let effectiveTools = this.config.tools
456
+ if (isSmallModelEnabled(this.config)) {
457
+ const reduced = selectRelevantTools(this.config.tools, this.currentUserInput, { enable: true })
458
+ effectiveTools = reduced
459
+ if (this.config.verbose && reduced.length !== this.config.tools.length) {
460
+ console.error(`[small-model] 按意图精简工具:${this.config.tools.length} → ${reduced.length}(${reduced.map(t => t.name).join(', ')})`)
461
+ }
462
+ }
463
+ const tools = effectiveTools.map(t => ({
394
464
  type: 'function',
395
465
  function: { name: t.name, description: t.description, parameters: t.parameters },
396
466
  }))
@@ -0,0 +1,250 @@
1
+ /**
2
+ * 小模型适配层 — 让 cc-node 在弱模型(如 27B Q3 量化)下也能可靠工作
3
+ *
4
+ * 背景:
5
+ * 小模型的核心弱点是"规划 + 工具调用 + 自我纠错"不稳定——常出现:
6
+ * - 看不懂指令,只回 "Solved by sharing best practices." 等敷衍空话,不调工具;
7
+ * - 一次请求里塞 16 个工具定义让它选择困难,乱调或漏调;
8
+ * - 多轮工具往返后"丢"了原始目标。
9
+ * 本模块把智能从"模型端"转移到"框架端",提供两层适配:
10
+ *
11
+ * 【层 A — 兜底(防呆)】:
12
+ * - 强化 system prompt:明确告诉模型"必须调用工具完成任务,不能只回文字";
13
+ * - 敷衍输出检测:识别无工具调用 + 无实质内容的空话,返回需要重试的信号;
14
+ * - 工具数量精简:按任务场景只暴露核心工具子集,减少选择负担。
15
+ *
16
+ * 【层 B — 替代(框架接管规划)】:
17
+ * - 意图识别:用关键词规则把常见任务(写文件/找文件/跑命令/查代码)映射到
18
+ * 明确的工具动作,不完全依赖模型自主规划;
19
+ * - 提供一个"计划"数据结构,框架可据此按序执行。
20
+ *
21
+ * 注意:本模块默认【不启用】。通过 config.smallModel=true 或 --small-model 开启。
22
+ */
23
+
24
+ // ---- 敷衍/空话输出特征(层 A)----
25
+ // 小模型在"没读懂、没调工具"时输出的占位句/空话模式。
26
+ // 命中这些且【本轮无工具调用】时,判定为敷衍输出,触发重试。
27
+ const FILLER_PATTERNS = [
28
+ /^\s*solved/i,
29
+ /^\s*done\b/i,
30
+ /by sharing best practices/i,
31
+ /this is the kind of mistake/i,
32
+ /next action/i,
33
+ /^\s*ok\b/i,
34
+ /^\s*okay\b/i,
35
+ /^\s*got it/i,
36
+ /^\s*understood/i,
37
+ /^\s*no problem/i,
38
+ /^\s*let me think/i,
39
+ /^\s*sure\b/i,
40
+ /^\s*yes\b/i,
41
+ /^\s*right\b/i,
42
+ /^\s*hmm/i,
43
+ ]
44
+
45
+ // 有"实质内容"的最小长度(去掉空白/填充后)
46
+ const MIN_SUBSTANTIVE_CHARS = 20
47
+
48
+ /**
49
+ * 判断一次模型回复是否属于"敷衍输出"(无工具调用 + 空话/过短)
50
+ *
51
+ * 用途:小模型模式下,当模型没调工具却回了句空话时,判定为"没干活",
52
+ * 触发框架重试(并在重试时追加强引导)。
53
+ *
54
+ * @param {object} response — _callLLM 的返回 { content, toolCalls, ... }
55
+ * @param {object} [opts]
56
+ * @param {number} [opts.minChars=20] — 判定"实质内容"的最小有效字符数
57
+ * @returns {boolean} true 表示敷衍输出(应重试)
58
+ */
59
+ export function isFillerResponse(response, opts = {}) {
60
+ const minChars = opts.minChars || MIN_SUBSTANTIVE_CHARS
61
+ // 有工具调用 → 在干活,不是敷衍
62
+ if (response.toolCalls && response.toolCalls.length > 0) return false
63
+ const content = (response.content || '').trim()
64
+ if (!content) return true // 空回复 = 敷衍
65
+ // 去掉空白后的有效字符数
66
+ const substantive = content.replace(/\s+/g, '').replace(/[,。!?,.!?、;;::]/g, '')
67
+ if (substantive.length < minChars) return true // 太短 = 敷衍
68
+ // 命中空话模式
69
+ for (const pat of FILLER_PATTERNS) {
70
+ if (pat.test(content)) return true
71
+ }
72
+ return false
73
+ }
74
+
75
+ // ---- 小模型强化 system prompt(层 A)----
76
+ // 追加到基础 system prompt 之后的强工具引导。对小模型是决定性的:
77
+ // 大模型能"猜"到要调工具,小模型必须被明确告知。
78
+ export const SMALL_MODEL_SYSTEM_PROMPT = `
79
+ ## 工具使用铁律(对你至关重要)
80
+ 你是一个【工具调用 Agent】,不是一个聊天机器人。用户给你的是一个【任务】,你必须通过调用工具来完成任务,绝对不能只回复文字。
81
+
82
+ 遵守以下规则:
83
+ 1. **每次收到用户任务,第一反应是"该调用哪个工具",而不是"该怎么用文字回答"。**
84
+ 2. 如果任务需要读取/查看/搜索文件,调用 Glob / Grep / Read。
85
+ 3. 如果任务需要创建/修改文件,调用 Write / Edit。
86
+ 4. 如果任务需要运行命令,调用 Bash。
87
+ 5. 如果任务需要联网,调用 WebFetch / WebSearch。
88
+ 6. **调用工具时,必须提供完整、正确的参数**(绝对路径、完整内容、具体命令)。
89
+ 7. 调用工具后,根据工具返回结果继续,直到任务真正完成。
90
+ 8. 除非任务只是简单问答(不需要任何工具),否则【禁止】不调用工具就回复。
91
+ 9. 禁止输出 "Solved by..."、"\u201cDone\u201d"、"Let me think" 等空话——这些不是完成任务。
92
+ 10. 如果一次要做多件事,一次调用一个工具,逐步推进,不要试图一次性解决。
93
+
94
+ 记住:**你的价值在于调用工具把事做完,而不是说漂亮话。**
95
+ `
96
+
97
+ /**
98
+ * 生成小模型模式下的完整 system prompt(基础 prompt + 强化引导)
99
+ * @param {string} basePrompt — 基础 system prompt
100
+ * @returns {string}
101
+ */
102
+ export function buildSmallModelSystemPrompt(basePrompt) {
103
+ return basePrompt + SMALL_MODEL_SYSTEM_PROMPT
104
+ }
105
+
106
+ // ---- 重试追加强引导(层 A)----
107
+ // 当模型敷衍输出触发重试时,往对话里注入一条"强制工具调用"的用户级提醒,
108
+ // 让模型看到"我刚才敷衍了,现在必须调工具"。
109
+ export const RETRY_GUIDANCE = `[系统提示] 你刚才没有调用任何工具就回复了,这不算完成任务。请立即重新审视任务,调用合适的工具(Write/Edit/Read/Grep/Glob/Bash 等)真正完成任务。不要再输出空话。`
110
+
111
+ // ---- 意图识别 + 框架动作映射(层 B)----
112
+ // 小模型模式下,框架用关键词规则把常见任务映射到明确的工具动作,
113
+ // 作为"计划"注入,引导模型按序执行(而非让模型自由发挥)。
114
+ //
115
+ // 每个意图规则:
116
+ // pattern: 触发正则
117
+ // plan: 建议的工具调用序列(按序执行)
118
+ // note: 给模型的动作说明
119
+
120
+ const INTENT_RULES = [
121
+ {
122
+ // 读/查看/阅读文档或文件 → 读取任务(优先于写规则,避免"阅读XX文档"被误判为写)
123
+ pattern: /(读|阅读|查看|打开|展示|显示|看看|浏览).*(文档|文件|内容|md|readme|代码|计划)/i,
124
+ plan: ['Read(读文件内容)'],
125
+ note: '这是读取任务,用 Read 读取目标文件内容并理解。',
126
+ toolHint: 'Read, Glob',
127
+ },
128
+ {
129
+ // 写文档/文件/计划/代码:覆盖"写/创建/生成/更新/保存/做好...文档/计划"等
130
+ pattern: /(写|创建|生成|更新|保存|做好|做一份|制定|起草|撰写|输出|整理|编写).*(文档|文件|计划|方案|说明|md|readme|代码|函数|模块|接口)/i,
131
+ plan: ['Grep/Glob(先看现状)', 'Write/Edit(写内容)'],
132
+ note: '先搜索确认目标文件是否存在/已有内容,再用 Write 或 Edit 写入。',
133
+ toolHint: 'Write, Edit, Glob, Grep',
134
+ },
135
+ {
136
+ // 明确提到"文档/文件"但未命中动词 → 归为写文件任务("做...开发计划文档")
137
+ pattern: /(文档|计划文档|开发计划|方案文档|说明文档|md文件).*(写好|完成|制作|做|实现|产出)?$/i,
138
+ plan: ['Write/Edit(写内容)'],
139
+ note: '这是一个要产出文档/计划的写作任务,直接 Write 或 Edit 写入目标文件。',
140
+ toolHint: 'Write, Edit',
141
+ },
142
+ {
143
+ pattern: /(找|搜索|查|列出|看看|查看|浏览).*(文件|代码|函数|目录|项目|结构)/i,
144
+ plan: ['Glob(找文件)', 'Grep(搜内容)', 'Read(读文件)'],
145
+ note: '用 Glob 找文件路径,用 Grep 搜内容,用 Read 读文件内容。',
146
+ toolHint: 'Glob, Grep, Read',
147
+ },
148
+ {
149
+ pattern: /(跑|运行|执行|测试|调试|编译|构建).*(命令|脚本|程序|测试|npm|node|python|项目|build)/i,
150
+ plan: ['Bash(运行命令)'],
151
+ note: '用 Bash 运行命令/脚本/测试,并读取输出。',
152
+ toolHint: 'Bash',
153
+ },
154
+ {
155
+ pattern: /(改|修|修复|编辑|更新|优化|重构).*(文件|代码|bug|问题|错误|逻辑|接口|函数)/i,
156
+ plan: ['Read(读当前内容)', 'Edit(精确修改)'],
157
+ note: '先 Read 查看当前内容,再用 Edit 精确修改指定位置。',
158
+ toolHint: 'Read, Edit',
159
+ },
160
+ {
161
+ pattern: /(联网|搜索|查一下|网页|网络|资料|信息|最新|资讯)/i,
162
+ plan: ['WebSearch(搜索)', 'WebFetch(抓网页)'],
163
+ note: '用 WebSearch 搜索,用 WebFetch 抓取具体网页内容。',
164
+ toolHint: 'WebSearch, WebFetch',
165
+ },
166
+ {
167
+ pattern: /(git|提交|push|pull|commit|pr|github|版本)/i,
168
+ plan: ['Bash(git 命令)', 'GitTool(PR 管理)'],
169
+ note: '用 Bash 执行 git 命令,或用 GitTool 管理 GitHub PR。',
170
+ toolHint: 'Bash, GitTool',
171
+ },
172
+ ]
173
+
174
+ /**
175
+ * 根据用户指令识别意图,返回框架建议的动作计划
176
+ *
177
+ * @param {string} userInput — 用户指令
178
+ * @returns {{ matched: boolean, intent: string, note: string, toolHint: string } | null}
179
+ */
180
+ export function detectIntent(userInput) {
181
+ if (!userInput) return null
182
+ for (const rule of INTENT_RULES) {
183
+ if (rule.pattern.test(userInput)) {
184
+ return {
185
+ matched: true,
186
+ intent: rule.plan.join(' → '),
187
+ note: rule.note,
188
+ toolHint: rule.toolHint,
189
+ }
190
+ }
191
+ }
192
+ return null
193
+ }
194
+
195
+ /**
196
+ * 生成注入到消息里的"意图引导 system 提示"(层 B)
197
+ *
198
+ * 当检测到明确意图时,往上下文中插入一条引导,告诉模型该用哪些工具,
199
+ * 降低小模型的决策负担。
200
+ *
201
+ * @param {string} userInput
202
+ * @param {object} [opts]
203
+ * @param {boolean} [opts.enable=true] — 是否启用意图引导
204
+ * @returns {string|null} 引导文本;未匹配或未启用返回 null
205
+ */
206
+ export function buildIntentGuidance(userInput, opts = {}) {
207
+ if (opts.enable === false) return null
208
+ const intent = detectIntent(userInput)
209
+ if (!intent) return null
210
+ return `[任务引导] 根据用户指令,建议按此思路用工具推进:${intent.intent}。\n说明:${intent.note}\n可用工具:${intent.toolHint}`
211
+ }
212
+
213
+ /**
214
+ * 精简工具列表(层 A)— 小模型一次看太多工具会混乱
215
+ *
216
+ * 从完整工具列表里,按用户指令筛选出最相关的核心工具子集。
217
+ * 这样模型的工具选择负担小,更不容易乱调/漏调。
218
+ *
219
+ * @param {Array} allTools — 完整 ToolDef 列表
220
+ * @param {string} userInput — 用户指令
221
+ * @param {object} [opts]
222
+ * @param {boolean} [opts.enable=true] — 是否启用精简
223
+ * @returns {Array} 精简后的工具列表
224
+ */
225
+ export function selectRelevantTools(allTools, userInput, opts = {}) {
226
+ if (opts.enable === false) return allTools
227
+ if (!allTools || allTools.length === 0) return allTools
228
+
229
+ const intent = detectIntent(userInput)
230
+ // 未识别到明确意图 → 返回核心工具(不塞满 16 个)
231
+ const coreNames = intent
232
+ ? intent.toolHint.split(',').map(s => s.trim())
233
+ : ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep']
234
+
235
+ // 核心工具优先,保留指定工具;若一个都没命中则退回首屏核心集
236
+ const selected = allTools.filter(t => coreNames.includes(t.name))
237
+ if (selected.length === 0) {
238
+ return allTools.filter(t => ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep'].includes(t.name))
239
+ }
240
+ return selected
241
+ }
242
+
243
+ /**
244
+ * 小模型模式开关判断
245
+ * @param {object} engineConfig — QueryEngineConfig 实例
246
+ * @returns {boolean}
247
+ */
248
+ export function isSmallModelEnabled(engineConfig) {
249
+ return !!(engineConfig && engineConfig.smallModel)
250
+ }