Commit 746c5b0a by luoqi

fix(ai-script): qwen3.7-max 结构化输出修复(DashScope 三件套)

接入后报错'No object generated: response did not match schema'。逐项查实 DashScope compatible-mode 三个前提:
1. supportsStructuredOutputs:true — SDK 才发 response_format=json_schema(DashScope 实测严格按 schema 返;
   松散 json_object 不带 schema → qwen 乱返 → Zod 不符)。
2. prompt 含 'json' 字样 — DashScope 硬性校验(否则 400 InvalidParameter);runner 对 qwen 注入(withQwenJsonHint)。
3. enable_thinking:false — qwen3.7-max 是推理模型,流式下 thinking 污染 content 流 → JSON 解析碎;
   SDK 无此参数,用 createOpenAICompatible 的 fetch 中间件往 body 注入。关思考还顺带提速、降本(¥0.031→0.008)。
验证:standard 流式 source=agent、succeeded、¥0.0078。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 6dc6a790
......@@ -89,7 +89,9 @@ export class AiCallRunnerService {
}
// ─── 2. 起 invocation ───
const { system, prompt } = call.buildPrompt(input);
const built = call.buildPrompt(input);
const prompt = built.prompt;
const system = withQwenJsonHint(provider, built.system); // qwen(DashScope)结构化输出要求 prompt 含 "json"
const invocationId = await this.recorder.start({
...this.baseStart(call, ctx, modelId, provider, inputHash, input, false),
promptTemplate: prompt.length > 8000 ? prompt.slice(0, 8000) + '…[truncated]' : prompt,
......@@ -205,7 +207,9 @@ export class AiCallRunnerService {
const inputHash = computeInputHash(call.callKey, call.promptVersion, input);
const requestedModelId = ctx.modelIdOverride ?? call.defaultModelId;
const { model, provider, modelId } = this.provider.resolve(requestedModelId);
const { system, prompt } = call.buildPrompt(input);
const built = call.buildPrompt(input);
const prompt = built.prompt;
const system = withQwenJsonHint(provider, built.system); // qwen(DashScope)结构化输出要求 prompt 含 "json"
const invocationId = await this.recorder.start({
...this.baseStart(call, ctx, modelId, provider, inputHash, input, false),
......@@ -442,3 +446,14 @@ export class AiCallRunnerService {
return Math.max(0, yuan);
}
}
/**
* qwen(DashScope compatible-mode)用 json_schema 结构化输出时,DashScope 硬性要求 prompt 出现 "json" 字样,
* 否则 400(InvalidParameter: messages must contain the word 'json')。仅对 qwen 在 system 末尾补一句;
* 其它 provider 原样返回。已含 json 字样则不重复加。
*/
function withQwenJsonHint(provider: string, system: string): string {
if (provider !== 'qwen') return system;
if (/json/i.test(system)) return system;
return `${system}\n\n请严格以 JSON 格式输出结果。`;
}
......@@ -54,11 +54,29 @@ export class AiProviderService {
if (!ai.qwenApiKey) {
this.logger.warn('QWEN_API_KEY(或 DASHSCOPE_API_KEY)未设置 — Qwen 调用会失败。到阿里云百炼 dashscope 申请');
}
// 通义千问走 DashScope OpenAI-兼容端点(compatible-mode);structured output 用 JSON 模式
// 通义千问走 DashScope OpenAI-兼容端点(compatible-mode)。
// DashScope 支持 OpenAI json_schema 严格模式(实测严格按 schema 返),但两个前提:
// ① supportsStructuredOutputs: true → SDK 才发 response_format=json_schema(否则松散 json_object 不符 schema)
// ② prompt 里必须出现 "json" 字样(DashScope 硬性校验)— 由 runner 对 qwen 注入(见 buildQwenSystem)
this.qwen = createOpenAICompatible({
name: 'qwen',
apiKey: ai.qwenApiKey,
baseURL: ai.qwenBaseUrl,
supportsStructuredOutputs: true,
// qwen3.7-max 是推理模型:流式下 thinking 会污染 content 流 → 结构化 JSON 解析失败。
// DashScope 用 body 里 enable_thinking:false 关思考(SDK 无此参数,用 fetch 中间件注入)。
fetch: (async (url: string | URL | Request, options?: RequestInit) => {
if (options?.body && typeof options.body === 'string') {
try {
const b = JSON.parse(options.body) as Record<string, unknown>;
b.enable_thinking = false;
options = { ...options, body: JSON.stringify(b) };
} catch {
/* 非 JSON body 不动 */
}
}
return fetch(url, options);
}) as typeof fetch,
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment