@raolin2025/claude-code-node 2.8.22 → 2.8.24
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/package.json +1 -1
- package/src/__tests__/small-model.test.js +12 -3
- package/src/core/cli.js +2 -2
- package/src/core/query-engine.js +17 -152
- package/src/core/small-model.js +35 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raolin2025/claude-code-node",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.24",
|
|
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",
|
|
@@ -127,10 +127,19 @@ test('工具精简:未启用返回全部', () => {
|
|
|
127
127
|
assert.equal(selectRelevantTools(fakeTools, 'xx', { enable: false }), fakeTools)
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
-
test('
|
|
130
|
+
test('工具精简:无明确意图时返回全部工具(让模型自己挑)', () => {
|
|
131
131
|
const reduced = selectRelevantTools(fakeTools, '你好', { enable: true })
|
|
132
|
-
|
|
133
|
-
assert.
|
|
132
|
+
// 实测经验:小模型不能过度精简工具,无明确适配时干脆全部给
|
|
133
|
+
assert.equal(reduced.length, fakeTools.length, '无明确意图应返回全部工具')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('工具精简:有明确意图时至少保留 6 个核心工具', () => {
|
|
137
|
+
const reduced = selectRelevantTools(fakeTools, '阅读D:\\x\\COMPLETION_REPORT.md', { enable: true })
|
|
138
|
+
// 至少 6 个核心工具,绝不精简到 2 个
|
|
139
|
+
assert.ok(reduced.length >= 6, `应至少保留 6 个工具(实际 ${reduced.length})`)
|
|
140
|
+
assert.ok(reduced.some(t => t.name === 'Read'))
|
|
141
|
+
assert.ok(reduced.some(t => t.name === 'Bash'), '应保留 Bash')
|
|
142
|
+
assert.ok(reduced.some(t => t.name === 'Write'), '应保留 Write')
|
|
134
143
|
})
|
|
135
144
|
|
|
136
145
|
// ---- system prompt 强化 ----
|
package/src/core/cli.js
CHANGED
|
@@ -374,9 +374,9 @@ Options:
|
|
|
374
374
|
-v, --verbose Verbose mode
|
|
375
375
|
--no-stream Disable streaming
|
|
376
376
|
--max-messages N Fold history when message count exceeds N (default: 0 = off)
|
|
377
|
-
--small-model
|
|
377
|
+
--small-model Reserved flag (no-op now — tool engine uses plain mode like v2.8.11)
|
|
378
378
|
--max-output-tokens N Override max single-response output tokens (default: computed from window size)
|
|
379
|
-
--small-model-max-turns N
|
|
379
|
+
--small-model-max-turns N Reserved (no-op — tool loop uses maxTurns)
|
|
380
380
|
--with-notify Start built-in channel listener (Telegram)
|
|
381
381
|
(replaces cc-notify daemon — no external script needed)
|
|
382
382
|
-h, --help Show this help
|
package/src/core/query-engine.js
CHANGED
|
@@ -15,15 +15,6 @@ 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
|
-
buildCombinedGuidance,
|
|
23
|
-
selectRelevantTools,
|
|
24
|
-
RETRY_GUIDANCE,
|
|
25
|
-
extractFrameworkAction,
|
|
26
|
-
} from './small-model.js'
|
|
27
18
|
import { CostTracker } from './cost-tracker.js'
|
|
28
19
|
import { EnhancedPermissionChecker } from '../security/enhanced-permission.js'
|
|
29
20
|
import { isLocalLlmServer, buildAuthHeaders } from '../utils/index.js'
|
|
@@ -97,8 +88,6 @@ export class QueryEngine {
|
|
|
97
88
|
this.tokenBudget = this.config.tokenBudget || null
|
|
98
89
|
// 最近一次 processMessage 是否已通过流式回调/直写把正文输出(供调用方避免重复打印 response)
|
|
99
90
|
this.lastStreamed = false
|
|
100
|
-
// 当前正在处理的用户输入(小模型模式用于按意图精简工具集)
|
|
101
|
-
this.currentUserInput = ''
|
|
102
91
|
}
|
|
103
92
|
|
|
104
93
|
/**
|
|
@@ -114,7 +103,6 @@ export class QueryEngine {
|
|
|
114
103
|
this.state.turnCount++
|
|
115
104
|
this.lastStreamed = false // 本轮是否已流式输出正文
|
|
116
105
|
this.abortController = new AbortController()
|
|
117
|
-
this.currentUserInput = userInput // 记录当前用户指令(小模型模式按意图精简工具)
|
|
118
106
|
const userMsg = new UserMessage(userInput, images)
|
|
119
107
|
this.state.messages.push(userMsg)
|
|
120
108
|
|
|
@@ -226,65 +214,17 @@ export class QueryEngine {
|
|
|
226
214
|
*/
|
|
227
215
|
async _runToolLoop(userMessage) {
|
|
228
216
|
let finalResponse = ''
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
// - 普通模式:用 config.maxTurns
|
|
238
|
-
const toolLoopLimit = smallModel
|
|
239
|
-
? (this.config.smallModelMaxTurns || 8)
|
|
240
|
-
: this.config.maxTurns
|
|
241
|
-
|
|
242
|
-
for (let turn = 0; turn < toolLoopLimit; turn++) {
|
|
217
|
+
|
|
218
|
+
// 朴素工具循环(v2.8.11 风格):
|
|
219
|
+
// 实测结论——小模型在"朴素模式"(16 个工具全上、无引导注入、无代执行、
|
|
220
|
+
// 无工具精简)下才能正常工作。v2.8.13 后我加的意图引导注入、工具精简、
|
|
221
|
+
// 多步计划、框架代执行等"辅助机制"反而让模型乱套(不调用工具、瞎编内容)。
|
|
222
|
+
// 因此这里【不做任何干预】,回归最原始行为:构建请求 → 调 LLM →
|
|
223
|
+
// 有工具调用就执行 → 循环。
|
|
224
|
+
for (let turn = 0; turn < this.config.maxTurns; turn++) {
|
|
243
225
|
// 发送前硬校验:工具结果可能已使上下文超窗,确保 ≤ 窗口(摘要优先 + 滑动窗口裁剪兜底)
|
|
244
226
|
this._ensureFitWindow()
|
|
245
227
|
|
|
246
|
-
// 小模型模式:首轮若识别到意图/多步计划,把引导【追加到首条 system 消息】。
|
|
247
|
-
// 注意:不能 push 一条新的 system 消息到中间——llama.cpp/Jinja 严格要求
|
|
248
|
-
// system 消息必须在对话开头,中间插 system 会报 "System message must be at the beginning" (500)。
|
|
249
|
-
if (smallModel && !intentGuided && this.state.messages.length > 0) {
|
|
250
|
-
const guidance = buildCombinedGuidance(userMessage.content, {
|
|
251
|
-
enable: true,
|
|
252
|
-
cwd: this.config.cwd,
|
|
253
|
-
})
|
|
254
|
-
if (guidance) {
|
|
255
|
-
// 找到第一条 system 消息,把引导拼接进去(保持 system 全在开头)
|
|
256
|
-
const sysIdx = this.state.messages.findIndex(m => m.role === 'system')
|
|
257
|
-
if (sysIdx !== -1) {
|
|
258
|
-
const sysMsg = this.state.messages[sysIdx]
|
|
259
|
-
this.state.messages[sysIdx] = {
|
|
260
|
-
...sysMsg,
|
|
261
|
-
content: (typeof sysMsg.content === 'string' ? sysMsg.content : '') + '\n\n' + guidance,
|
|
262
|
-
}
|
|
263
|
-
} else {
|
|
264
|
-
// 没有 system 消息 → 作为 user 引导插入(跟随在最新 user 之后作为新 user)
|
|
265
|
-
this.state.messages.push({ role: 'user', content: guidance })
|
|
266
|
-
}
|
|
267
|
-
if (this.config.verbose) console.error('[small-model] 已注入引导(并入首条 system):' + guidance.slice(0, 60) + '...')
|
|
268
|
-
}
|
|
269
|
-
intentGuided = true
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// 小模型模式:接近工具循环上限时,注入"总结收尾"引导,防止模型无限循环
|
|
273
|
-
if (smallModel && turn === toolLoopLimit - 1) {
|
|
274
|
-
const stopGuidance = `[系统提示] 你已经完成了大部分工作。现在请【停止调用新工具】,把已经完成的内容整理成一段总结回复给用户。如果确实还有关键步骤未完成,只再调用一次工具完成它,然后立即总结。不要再继续无休止地调用工具。`
|
|
275
|
-
const sysIdx = this.state.messages.findIndex(m => m.role === 'system')
|
|
276
|
-
if (sysIdx !== -1) {
|
|
277
|
-
const sysMsg = this.state.messages[sysIdx]
|
|
278
|
-
this.state.messages[sysIdx] = {
|
|
279
|
-
...sysMsg,
|
|
280
|
-
content: (typeof sysMsg.content === 'string' ? sysMsg.content : '') + '\n\n' + stopGuidance,
|
|
281
|
-
}
|
|
282
|
-
} else {
|
|
283
|
-
this.state.messages.push({ role: 'user', content: stopGuidance })
|
|
284
|
-
}
|
|
285
|
-
if (this.config.verbose) console.error(`[small-model] 已到工具循环第 ${turn + 1} 轮(上限 ${toolLoopLimit}),注入收尾引导`)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
228
|
const requestMessages = this._buildRequest(this.state.messages)
|
|
289
229
|
const response = await this._callLLM(requestMessages, this.state.messages)
|
|
290
230
|
|
|
@@ -292,32 +232,8 @@ export class QueryEngine {
|
|
|
292
232
|
throw new Error('操作已取消')
|
|
293
233
|
}
|
|
294
234
|
|
|
295
|
-
// 没有工具调用 →
|
|
235
|
+
// 没有工具调用 → 最终回复
|
|
296
236
|
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
297
|
-
// 小模型模式:检测敷衍输出(空话/太短/命中占位句)
|
|
298
|
-
if (smallModel && isFillerResponse(response)) {
|
|
299
|
-
// 第一次敷衍 → 追加强引导重试
|
|
300
|
-
if (fillerRetries < MAX_FILLER_RETRIES) {
|
|
301
|
-
fillerRetries++
|
|
302
|
-
if (this.config.verbose) {
|
|
303
|
-
console.error(`[small-model] 检测到敷衍输出(第 ${fillerRetries} 次),追加强引导重试`)
|
|
304
|
-
}
|
|
305
|
-
this.state.messages.push(new AssistantMessage(response.content, [], response.reasoningContent))
|
|
306
|
-
this.state.messages.push({ role: 'user', content: RETRY_GUIDANCE })
|
|
307
|
-
continue
|
|
308
|
-
}
|
|
309
|
-
// 重试后仍敷衍 → 框架代执行,且【直接把结果作为最终答案】返回,
|
|
310
|
-
// 不再 continue 让模型总结——模型已证明它只会敷衍,继续循环只会无限重复。
|
|
311
|
-
// 这样既完成任务,又不会陷入"代执行→敷衍→再代执行"的死循环。
|
|
312
|
-
const execResult = await this._frameworkExecute(userMessage.content)
|
|
313
|
-
if (execResult.done) {
|
|
314
|
-
if (this.config.verbose) console.error(`[small-model] 框架代执行完成,直接返回结果`)
|
|
315
|
-
// 框架已执行并把结果注入对话;直接把真实结果作为最终回复
|
|
316
|
-
finalResponse = execResult.resultText
|
|
317
|
-
break
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
237
|
finalResponse = response.content
|
|
322
238
|
this.state.messages.push(new AssistantMessage(response.content, [], response.reasoningContent))
|
|
323
239
|
break
|
|
@@ -344,9 +260,8 @@ export class QueryEngine {
|
|
|
344
260
|
}
|
|
345
261
|
}
|
|
346
262
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
finalResponse = `[已达到工具循环上限 (${toolLoopLimit}),已停止进一步调用工具。请查看上方工具执行结果,确认任务完成情况。]`
|
|
263
|
+
if (!finalResponse && this.state.turnCount >= this.config.maxTurns) {
|
|
264
|
+
finalResponse = `[达到最大回合数限制 (${this.config.maxTurns}),停止响应]`
|
|
350
265
|
}
|
|
351
266
|
|
|
352
267
|
return {
|
|
@@ -388,12 +303,10 @@ export class QueryEngine {
|
|
|
388
303
|
_buildRequest(messages) {
|
|
389
304
|
const request = []
|
|
390
305
|
|
|
391
|
-
//
|
|
306
|
+
// 系统提示(朴素模式:直接用基础 system prompt,不做 cwd/工具铁律等注入——
|
|
307
|
+
// 实测 v2.8.11 朴素模式能正常工作,注入反而干扰模型)
|
|
392
308
|
if (this.config.systemPrompt) {
|
|
393
|
-
|
|
394
|
-
? buildSmallModelSystemPrompt(this.config.systemPrompt, { cwd: this.config.cwd })
|
|
395
|
-
: this.config.systemPrompt
|
|
396
|
-
request.push({ role: 'system', content: sysContent })
|
|
309
|
+
request.push({ role: 'system', content: this.config.systemPrompt })
|
|
397
310
|
}
|
|
398
311
|
|
|
399
312
|
// 历史消息 — 转换为 OpenAI 兼容格式
|
|
@@ -512,47 +425,6 @@ export class QueryEngine {
|
|
|
512
425
|
return results
|
|
513
426
|
}
|
|
514
427
|
|
|
515
|
-
/**
|
|
516
|
-
* 框架代执行 — 模型敷衍调不起工具时,框架直接替它执行最合理的工具
|
|
517
|
-
*
|
|
518
|
-
* 背景:27B Q3 量化模型工具调用极弱,即使强引导也常只回"研究一下"而不调工具。
|
|
519
|
-
* 此时框架【绕过模型】,根据指令推断该执行什么工具并直接运行,把真实结果
|
|
520
|
-
* 注入对话,再让模型基于结果总结——"一句话调用小模型工作"才能成立。
|
|
521
|
-
*
|
|
522
|
-
* @param {string} userInput — 用户指令
|
|
523
|
-
* @returns {Promise<{ done: boolean, summary: string }>}
|
|
524
|
-
* done=true 表示框架已执行工具(结果已注入 state.messages)
|
|
525
|
-
*/
|
|
526
|
-
async _frameworkExecute(userInput) {
|
|
527
|
-
const action = extractFrameworkAction(userInput, { cwd: this.config.cwd })
|
|
528
|
-
if (!action) return { done: false, summary: '', resultText: '' }
|
|
529
|
-
|
|
530
|
-
// 找到对应的工具
|
|
531
|
-
const tool = this.config.tools.find(t => t.name === action.tool)
|
|
532
|
-
if (!tool) return { done: false, summary: '', resultText: '' }
|
|
533
|
-
|
|
534
|
-
// 执行工具(直接调用 handler)
|
|
535
|
-
try {
|
|
536
|
-
const content = await tool.handler(action.input, { cwd: this.config.cwd, engine: this, readline: this.config.readline })
|
|
537
|
-
|
|
538
|
-
const resultText = typeof content === 'string' ? content : JSON.stringify(content)
|
|
539
|
-
|
|
540
|
-
// 注入框架代执行结果(作为 user 消息,避免 tool 消息无对应 assistant tool_call 报错)
|
|
541
|
-
// 说明:框架代执行不是模型发起的工具调用,不能作为 tool 角色(会缺 assistant tool_call)。
|
|
542
|
-
this.state.messages.push({
|
|
543
|
-
role: 'user',
|
|
544
|
-
content: `[框架代执行结果] 框架已替你执行工具 ${tool.name}(参数 ${JSON.stringify(action.input)}),结果如下:\n\n${resultText.slice(0, 8000)}`,
|
|
545
|
-
})
|
|
546
|
-
|
|
547
|
-
const summary = `框架已代执行 ${tool.name}(${JSON.stringify(action.input)})`
|
|
548
|
-
if (this.config.verbose) console.error(`[small-model] 框架代执行 ${tool.name} 完成`)
|
|
549
|
-
return { done: true, summary, resultText }
|
|
550
|
-
} catch (err) {
|
|
551
|
-
if (this.config.verbose) console.error(`[small-model] 框架代执行失败: ${err.message}`)
|
|
552
|
-
return { done: false, summary: '', resultText: '' }
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
428
|
async _callLLM(messages, contextMessages) {
|
|
557
429
|
const apiKey = this.config.apiKey
|
|
558
430
|
const apiBase = this.config.apiBase
|
|
@@ -580,16 +452,9 @@ export class QueryEngine {
|
|
|
580
452
|
}
|
|
581
453
|
|
|
582
454
|
// 构建工具定义
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const reduced = selectRelevantTools(this.config.tools, this.currentUserInput, { enable: true })
|
|
587
|
-
effectiveTools = reduced
|
|
588
|
-
if (this.config.verbose && reduced.length !== this.config.tools.length) {
|
|
589
|
-
console.error(`[small-model] 按意图精简工具:${this.config.tools.length} → ${reduced.length}(${reduced.map(t => t.name).join(', ')})`)
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
const tools = effectiveTools.map(t => ({
|
|
455
|
+
// 朴素模式(v2.8.11 风格):16 个工具全上,不做精简。
|
|
456
|
+
// 实测结论——小模型在工具全给时才能正常调用;按意图精简反而让它不会调用、瞎编。
|
|
457
|
+
const tools = this.config.tools.map(t => ({
|
|
593
458
|
type: 'function',
|
|
594
459
|
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
595
460
|
}))
|
package/src/core/small-model.js
CHANGED
|
@@ -377,32 +377,53 @@ export function buildCombinedGuidance(userInput, opts = {}) {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
/**
|
|
380
|
-
*
|
|
380
|
+
* 工具选择(层 A)— 小模型适配的关键经验
|
|
381
381
|
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
382
|
+
* 重要结论(来自实测):**小模型工具调用不能过度精简**。
|
|
383
|
+
* v2.8.12 之前(16 个工具全上)模型能正常调用工具;v2.8.13 之后我做了"按意图
|
|
384
|
+
* 精简成 2-6 个",结果模型反而不调用工具、只会瞎编。因此策略改为:
|
|
385
|
+
*
|
|
386
|
+
* 1. **保底至少 6 个核心工具**:Bash, Read, Edit, Write, Glob, Grep(每次必给);
|
|
387
|
+
* 2. **按任务适配追加**:若意图明确需要额外工具(联网→WebSearch/WebFetch 等),
|
|
388
|
+
* 在核心 6 个之上追加,绝不减少核心集;
|
|
389
|
+
* 3. **找不到合适的适配,就全部给**:宁可全给 16 个,也不要精简到很少。
|
|
390
|
+
*
|
|
391
|
+
* 原则:**工具给全,让模型自己挑;而不是替它精简,导致它无工具可用而瞎编。**
|
|
384
392
|
*
|
|
385
393
|
* @param {Array} allTools — 完整 ToolDef 列表
|
|
386
394
|
* @param {string} userInput — 用户指令
|
|
387
395
|
* @param {object} [opts]
|
|
388
|
-
* @param {boolean} [opts.enable=true] —
|
|
389
|
-
* @returns {Array}
|
|
396
|
+
* @param {boolean} [opts.enable=true] — 是否启用适配
|
|
397
|
+
* @returns {Array} 选择后的工具列表(至少 6 个核心;找不到适配则全部)
|
|
390
398
|
*/
|
|
391
399
|
export function selectRelevantTools(allTools, userInput, opts = {}) {
|
|
392
400
|
if (opts.enable === false) return allTools
|
|
393
401
|
if (!allTools || allTools.length === 0) return allTools
|
|
394
402
|
|
|
403
|
+
// 核心工具保底集(每次必给):小模型不能过度精简工具,否则不会调用只会瞎编
|
|
404
|
+
const CORE = ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep']
|
|
395
405
|
const intent = detectIntent(userInput)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
// 核心工具优先,保留指定工具;若一个都没命中则退回首屏核心集
|
|
402
|
-
const selected = allTools.filter(t => coreNames.includes(t.name))
|
|
403
|
-
if (selected.length === 0) {
|
|
404
|
-
return allTools.filter(t => ['Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep'].includes(t.name))
|
|
406
|
+
|
|
407
|
+
// 找不到合适的适配(无明确意图)→ 干脆全部调用,让模型自己挑。
|
|
408
|
+
// 这是实测经验:v2.8.12 之前全 16 个工具模型能正常调用;精简后反而变傻。
|
|
409
|
+
if (!intent) {
|
|
410
|
+
return allTools
|
|
405
411
|
}
|
|
412
|
+
|
|
413
|
+
// 有明确意图 → 核心 6 个(保底)+ 意图命中的额外工具(追加,不减少核心)
|
|
414
|
+
const toolNames = new Set(CORE)
|
|
415
|
+
if (intent.toolHint) {
|
|
416
|
+
for (const name of intent.toolHint.split(',').map(s => s.trim())) {
|
|
417
|
+
toolNames.add(name)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const selected = allTools.filter(t => toolNames.has(t.name))
|
|
421
|
+
|
|
422
|
+
// 兜底:筛选不足 6 个(核心工具不全)→ 全给
|
|
423
|
+
if (selected.length < 6) {
|
|
424
|
+
return allTools
|
|
425
|
+
}
|
|
426
|
+
|
|
406
427
|
return selected
|
|
407
428
|
}
|
|
408
429
|
|