Skip to content

Commit 82c496b

Browse files
committed
fix(ai-chat): explain json failures and widen judge
The interjection judge capped output at 300 tokens, which reasoning models can spend on thinking before writing any JSON, so judging kept failing with a bare "no valid JSON" message. Raise the cap to 1024 and make JSON failures report finish_reason, a short output sample and the number of tries, so truncation and empty replies show in the log.
1 parent 36a36af commit 82c496b

3 files changed

Lines changed: 22 additions & 2 deletions

File tree

‎src/plugins/ai-chat/llm-client.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,14 @@ describe('isInputError', () => {
200200
expect(isInputError(new TypeError("Header 'Authorization' has invalid value"))).toBe(false);
201201
});
202202
});
203+
204+
describe('jsonCompletion 的失败信息', () => {
205+
test('被 maxTokens 截断时,错误里写明结束原因和输出片段', async () => {
206+
scriptFetch([[200, { choices: [{ message: { content: '{"action":"re' }, finish_reason: 'length' }] }]]);
207+
const err = await jsonCompletion({ ...baseReq, purpose: 'chat', repairAttempts: 0, validate: () => null }).catch((e) => e);
208+
expect(err).toBeInstanceOf(JsonOutputError);
209+
expect((err as Error).message).toContain('finish_reason=length');
210+
expect((err as Error).message).toContain('截断');
211+
expect((err as Error).message).toContain('{"action":"re');
212+
});
213+
});

‎src/plugins/ai-chat/llm-client.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,9 @@ export async function jsonCompletion<T>(req: JsonRequest<T>): Promise<T> {
331331
const messages: ChatMessage[] = [...base.messages];
332332
const key = `${base.target.apiUrl}|${base.target.model}`;
333333

334+
// 最后一次的输出与结束原因:失败时写进错误,日志里能看出是被截断、空回复还是答非所问
335+
let lastText = '';
336+
let lastFinish = '';
334337
for (let round = 0; round <= repairAttempts; round++) {
335338
const useJsonMode = !jsonModeUnsupported.has(key);
336339
let data: ChatCompletionResponse;
@@ -355,6 +358,8 @@ export async function jsonCompletion<T>(req: JsonRequest<T>): Promise<T> {
355358
const parsed = parseJsonLoose(text);
356359
const value = parsed === undefined ? null : validate(parsed);
357360
if (value !== null) return value;
361+
lastText = text;
362+
lastFinish = data.choices?.[0]?.finish_reason || '';
358363

359364
if (round < repairAttempts) {
360365
logger.warn('AI', `${logPrefix(base.purpose)} 输出不是合格的 JSON,要求重新生成`);
@@ -367,7 +372,10 @@ export async function jsonCompletion<T>(req: JsonRequest<T>): Promise<T> {
367372
}
368373
}
369374

370-
throw new JsonOutputError('模型多次未返回符合要求的 JSON');
375+
const tries = repairAttempts + 1;
376+
const hint = lastFinish === 'length' ? ':输出被 maxTokens 截断(推理模型的思考也占 token)' : '';
377+
const sample = lastText ? `,输出:${lastText.replace(/\s+/g, ' ').slice(0, 120)}` : ',输出为空';
378+
throw new JsonOutputError(`模型${tries > 1 ? ` ${tries} 次都` : ''}没有返回符合要求的 JSON(finish_reason=${lastFinish || '无'}${hint}${sample})`);
371379
}
372380

373381
/** 测试专用:清空"不支持 response_format"的记忆 */

‎src/plugins/ai-chat/proactive.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,8 @@ export async function judge(persona: PersonaCard, scan: TriggerScan, now: number
369369
{ role: 'system', content: p.system },
370370
{ role: 'user', content: p.user },
371371
],
372-
maxTokens: 300,
372+
// 输出只是一小段 JSON,但推理模型的思考也算在 maxTokens 里:给小了会在写出 JSON 之前被截断
373+
maxTokens: 1024,
373374
temperature: 0.5,
374375
repairAttempts: 0,
375376
validate: (v) => validateJudge(v, p.refs, getAiConfig().proactive.allowReactOnly),

0 commit comments

Comments
 (0)