fix: 按 DeepSeek 文档为每种调用指定是否思考,修复分类器拿不到结果
deepseek-flash 默认先思考(reasoning_effort high),思考也占输出 token,思考模式下 temperature 无效。 分类器只给 10 个 token,全被思考用掉,正文为空,切到新后端后的提问都没有层级。 - 生成代码:开思考;引导:关思考,回得快;分类:关思考,temperature 0 才生效 - thinking 只发给 DeepSeek,豆包请求不变 - 实测 deepseek-flash:分类约 1.2s,引导约 1.5s;固定 68 条样本两次一致 65 条 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b765cbf1aa
commit
6b2ffbb3ec
3 files changed
+27
-15
No files matched your search
@@ -37,8 +37,8 @@ async function callLlm(content: string): Promise<number | null> {
|
|||||||
{ role: "system", content: CLASSIFY_SYSTEM_PROMPT },
|
{ role: "system", content: CLASSIFY_SYSTEM_PROMPT },
|
||||||
{ role: "user", content },
|
{ role: "user", content },
|
||||||
],
|
],
|
||||||
// 同一句提示词要得到同一个层级
|
// 只回一个数字,不用思考;关了思考 temperature 才生效,同一句提示词得到同一个层级
|
||||||
{ maxTokens: 10, temperature: 0, timeoutMs: 30_000 },
|
{ thinking: false, maxTokens: 10, temperature: 0, timeoutMs: 30_000 },
|
||||||
)
|
)
|
||||||
const match = /[1-6]/.exec(text)
|
const match = /[1-6]/.exec(text)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
|||||||
+18
-11
@@ -59,9 +59,12 @@ export interface ChatMessage {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepSeek(deepseek-flash)默认先思考再回答,思考的内容在 reasoning_content 里,也算输出 token;
|
||||||
|
// 思考模式下 temperature 不起作用。thinking 只发给 DeepSeek,豆包不传
|
||||||
interface CompletionOptions {
|
interface CompletionOptions {
|
||||||
model?: string
|
model?: string
|
||||||
stream: boolean
|
stream: boolean
|
||||||
|
thinking?: boolean
|
||||||
maxTokens?: number
|
maxTokens?: number
|
||||||
temperature?: number
|
temperature?: number
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
@@ -82,6 +85,7 @@ function buildRequest(messages: ChatMessage[], opts: CompletionOptions) {
|
|||||||
messages,
|
messages,
|
||||||
stream: opts.stream,
|
stream: opts.stream,
|
||||||
}
|
}
|
||||||
|
if (!ark && opts.thinking !== undefined) body.thinking = { type: opts.thinking ? "enabled" : "disabled" }
|
||||||
if (opts.maxTokens) body.max_tokens = opts.maxTokens
|
if (opts.maxTokens) body.max_tokens = opts.maxTokens
|
||||||
if (opts.temperature !== undefined) body.temperature = opts.temperature
|
if (opts.temperature !== undefined) body.temperature = opts.temperature
|
||||||
const apiKey = ark ? config.arkApiKey : config.llmApiKey
|
const apiKey = ark ? config.arkApiKey : config.llmApiKey
|
||||||
@@ -109,8 +113,7 @@ async function ensureOk(res: Response) {
|
|||||||
// 读取 OpenAI 兼容接口的 SSE 流,逐段产出文本
|
// 读取 OpenAI 兼容接口的 SSE 流,逐段产出文本
|
||||||
async function* streamCompletion(
|
async function* streamCompletion(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
model?: string,
|
{ model, thinking, signal }: { model?: string; thinking?: boolean; signal?: AbortSignal },
|
||||||
signal?: AbortSignal,
|
|
||||||
): AsyncGenerator<string> {
|
): AsyncGenerator<string> {
|
||||||
// 带思考的模型生成整页代码可能超过两分钟,只要一直有数据就不该中断
|
// 带思考的模型生成整页代码可能超过两分钟,只要一直有数据就不该中断
|
||||||
const idle = new AbortController()
|
const idle = new AbortController()
|
||||||
@@ -125,6 +128,7 @@ async function* streamCompletion(
|
|||||||
const res = await buildRequest(messages, {
|
const res = await buildRequest(messages, {
|
||||||
model,
|
model,
|
||||||
stream: true,
|
stream: true,
|
||||||
|
thinking,
|
||||||
signal: AbortSignal.any(signals),
|
signal: AbortSignal.any(signals),
|
||||||
timeoutMs: STREAM_MAX_MS,
|
timeoutMs: STREAM_MAX_MS,
|
||||||
})
|
})
|
||||||
@@ -141,6 +145,7 @@ async function* streamCompletion(
|
|||||||
if (data === "[DONE]") return true
|
if (data === "[DONE]") return true
|
||||||
const json = JSON.parse(data)
|
const json = JSON.parse(data)
|
||||||
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error))
|
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error))
|
||||||
|
// 思考阶段只有 delta.reasoning_content,不发给学生(它也会重置空闲计时)
|
||||||
const content = json.choices?.[0]?.delta?.content
|
const content = json.choices?.[0]?.delta?.content
|
||||||
if (content) yield content
|
if (content) yield content
|
||||||
}
|
}
|
||||||
@@ -187,29 +192,31 @@ export function userFacingError(e: unknown) {
|
|||||||
return status ? `AI 服务出错(${status}),请稍后重试` : "AI 服务出错,请稍后重试"
|
return status ? `AI 服务出错(${status}),请稍后重试` : "AI 服务出错,请稍后重试"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 生成整页代码:开着思考,质量优先
|
||||||
export function streamChat(history: ChatMessage[], model?: string, signal?: AbortSignal) {
|
export function streamChat(history: ChatMessage[], model?: string, signal?: AbortSignal) {
|
||||||
return streamCompletion(
|
return streamCompletion([{ role: "system", content: SYSTEM_PROMPT }, ...trimHistory(history)], {
|
||||||
[{ role: "system", content: SYSTEM_PROMPT }, ...trimHistory(history)],
|
model: chatModel(model),
|
||||||
chatModel(model),
|
thinking: true,
|
||||||
signal,
|
signal,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 引导只是简短追问,不用思考,回得快
|
||||||
export function streamGuidance(history: ChatMessage[], signal?: AbortSignal) {
|
export function streamGuidance(history: ChatMessage[], signal?: AbortSignal) {
|
||||||
return streamCompletion(
|
return streamCompletion([{ role: "system", content: GUIDANCE_SYSTEM_PROMPT }, ...history], {
|
||||||
[{ role: "system", content: GUIDANCE_SYSTEM_PROMPT }, ...history],
|
thinking: false,
|
||||||
undefined,
|
|
||||||
signal,
|
signal,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function complete(
|
export async function complete(
|
||||||
messages: ChatMessage[],
|
messages: ChatMessage[],
|
||||||
opts: { model?: string; maxTokens?: number; temperature?: number; timeoutMs?: number } = {},
|
opts: { model?: string; thinking?: boolean; maxTokens?: number; temperature?: number; timeoutMs?: number } = {},
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const res = await buildRequest(messages, {
|
const res = await buildRequest(messages, {
|
||||||
model: opts.model ?? config.llmModel,
|
model: opts.model ?? config.llmModel,
|
||||||
stream: false,
|
stream: false,
|
||||||
|
thinking: opts.thinking,
|
||||||
maxTokens: opts.maxTokens,
|
maxTokens: opts.maxTokens,
|
||||||
temperature: opts.temperature,
|
temperature: opts.temperature,
|
||||||
timeoutMs: opts.timeoutMs,
|
timeoutMs: opts.timeoutMs,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
extractCode,
|
extractCode,
|
||||||
parseGuidanceResponse,
|
parseGuidanceResponse,
|
||||||
streamChat,
|
streamChat,
|
||||||
|
streamGuidance,
|
||||||
trimHistory,
|
trimHistory,
|
||||||
userFacingError,
|
userFacingError,
|
||||||
} from "../src/services/llm"
|
} from "../src/services/llm"
|
||||||
@@ -77,12 +78,13 @@ test("CSV 转义", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("streamChat", () => {
|
describe("streamChat", () => {
|
||||||
test("不认识的模型换成默认模型,最后一行没有换行也能读到", async () => {
|
test("不认识的模型换成默认模型,最后一行没有换行也能读到;生成代码开思考,引导不开", async () => {
|
||||||
let model = ""
|
let model = ""
|
||||||
|
let thinking: unknown
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
port: 0,
|
port: 0,
|
||||||
async fetch(req) {
|
async fetch(req) {
|
||||||
model = ((await req.json()) as any).model
|
;({ model, thinking } = (await req.json()) as any)
|
||||||
const line = (s: string) => `data: ${JSON.stringify({ choices: [{ delta: { content: s } }] })}`
|
const line = (s: string) => `data: ${JSON.stringify({ choices: [{ delta: { content: s } }] })}`
|
||||||
// 一行拆在两个分块之间,最后一行不带换行
|
// 一行拆在两个分块之间,最后一行不带换行
|
||||||
const body = `${line("你好")}\n\n${line("世")}\n${line("界")}`
|
const body = `${line("你好")}\n\n${line("世")}\n${line("界")}`
|
||||||
@@ -108,6 +110,9 @@ describe("streamChat", () => {
|
|||||||
for await (const s of streamChat([{ role: "user", content: "hi" }], "deepseek-v4-flash-thinking")) out += s
|
for await (const s of streamChat([{ role: "user", content: "hi" }], "deepseek-v4-flash-thinking")) out += s
|
||||||
expect(out).toBe("你好世界")
|
expect(out).toBe("你好世界")
|
||||||
expect(model).toBe(config.llmModel)
|
expect(model).toBe(config.llmModel)
|
||||||
|
expect(thinking).toEqual({ type: "enabled" })
|
||||||
|
for await (const _ of streamGuidance([{ role: "user", content: "hi" }]));
|
||||||
|
expect(thinking).toEqual({ type: "disabled" })
|
||||||
} finally {
|
} finally {
|
||||||
Object.assign(config, saved)
|
Object.assign(config, saved)
|
||||||
server.stop(true)
|
server.stop(true)
|
||||||
|
|||||||
Reference in new issue
Block a user