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: "user", content },
|
||||
],
|
||||
// 同一句提示词要得到同一个层级
|
||||
{ maxTokens: 10, temperature: 0, timeoutMs: 30_000 },
|
||||
// 只回一个数字,不用思考;关了思考 temperature 才生效,同一句提示词得到同一个层级
|
||||
{ thinking: false, maxTokens: 10, temperature: 0, timeoutMs: 30_000 },
|
||||
)
|
||||
const match = /[1-6]/.exec(text)
|
||||
if (!match) {
|
||||
|
||||
+18
-11
@@ -59,9 +59,12 @@ export interface ChatMessage {
|
||||
content: string
|
||||
}
|
||||
|
||||
// DeepSeek(deepseek-flash)默认先思考再回答,思考的内容在 reasoning_content 里,也算输出 token;
|
||||
// 思考模式下 temperature 不起作用。thinking 只发给 DeepSeek,豆包不传
|
||||
interface CompletionOptions {
|
||||
model?: string
|
||||
stream: boolean
|
||||
thinking?: boolean
|
||||
maxTokens?: number
|
||||
temperature?: number
|
||||
signal?: AbortSignal
|
||||
@@ -82,6 +85,7 @@ function buildRequest(messages: ChatMessage[], opts: CompletionOptions) {
|
||||
messages,
|
||||
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.temperature !== undefined) body.temperature = opts.temperature
|
||||
const apiKey = ark ? config.arkApiKey : config.llmApiKey
|
||||
@@ -109,8 +113,7 @@ async function ensureOk(res: Response) {
|
||||
// 读取 OpenAI 兼容接口的 SSE 流,逐段产出文本
|
||||
async function* streamCompletion(
|
||||
messages: ChatMessage[],
|
||||
model?: string,
|
||||
signal?: AbortSignal,
|
||||
{ model, thinking, signal }: { model?: string; thinking?: boolean; signal?: AbortSignal },
|
||||
): AsyncGenerator<string> {
|
||||
// 带思考的模型生成整页代码可能超过两分钟,只要一直有数据就不该中断
|
||||
const idle = new AbortController()
|
||||
@@ -125,6 +128,7 @@ async function* streamCompletion(
|
||||
const res = await buildRequest(messages, {
|
||||
model,
|
||||
stream: true,
|
||||
thinking,
|
||||
signal: AbortSignal.any(signals),
|
||||
timeoutMs: STREAM_MAX_MS,
|
||||
})
|
||||
@@ -141,6 +145,7 @@ async function* streamCompletion(
|
||||
if (data === "[DONE]") return true
|
||||
const json = JSON.parse(data)
|
||||
if (json.error) throw new Error(json.error.message ?? JSON.stringify(json.error))
|
||||
// 思考阶段只有 delta.reasoning_content,不发给学生(它也会重置空闲计时)
|
||||
const content = json.choices?.[0]?.delta?.content
|
||||
if (content) yield content
|
||||
}
|
||||
@@ -187,29 +192,31 @@ export function userFacingError(e: unknown) {
|
||||
return status ? `AI 服务出错(${status}),请稍后重试` : "AI 服务出错,请稍后重试"
|
||||
}
|
||||
|
||||
// 生成整页代码:开着思考,质量优先
|
||||
export function streamChat(history: ChatMessage[], model?: string, signal?: AbortSignal) {
|
||||
return streamCompletion(
|
||||
[{ role: "system", content: SYSTEM_PROMPT }, ...trimHistory(history)],
|
||||
chatModel(model),
|
||||
return streamCompletion([{ role: "system", content: SYSTEM_PROMPT }, ...trimHistory(history)], {
|
||||
model: chatModel(model),
|
||||
thinking: true,
|
||||
signal,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// 引导只是简短追问,不用思考,回得快
|
||||
export function streamGuidance(history: ChatMessage[], signal?: AbortSignal) {
|
||||
return streamCompletion(
|
||||
[{ role: "system", content: GUIDANCE_SYSTEM_PROMPT }, ...history],
|
||||
undefined,
|
||||
return streamCompletion([{ role: "system", content: GUIDANCE_SYSTEM_PROMPT }, ...history], {
|
||||
thinking: false,
|
||||
signal,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export async function complete(
|
||||
messages: ChatMessage[],
|
||||
opts: { model?: string; maxTokens?: number; temperature?: number; timeoutMs?: number } = {},
|
||||
opts: { model?: string; thinking?: boolean; maxTokens?: number; temperature?: number; timeoutMs?: number } = {},
|
||||
): Promise<string> {
|
||||
const res = await buildRequest(messages, {
|
||||
model: opts.model ?? config.llmModel,
|
||||
stream: false,
|
||||
thinking: opts.thinking,
|
||||
maxTokens: opts.maxTokens,
|
||||
temperature: opts.temperature,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
extractCode,
|
||||
parseGuidanceResponse,
|
||||
streamChat,
|
||||
streamGuidance,
|
||||
trimHistory,
|
||||
userFacingError,
|
||||
} from "../src/services/llm"
|
||||
@@ -77,12 +78,13 @@ test("CSV 转义", () => {
|
||||
})
|
||||
|
||||
describe("streamChat", () => {
|
||||
test("不认识的模型换成默认模型,最后一行没有换行也能读到", async () => {
|
||||
test("不认识的模型换成默认模型,最后一行没有换行也能读到;生成代码开思考,引导不开", async () => {
|
||||
let model = ""
|
||||
let thinking: unknown
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
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 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
|
||||
expect(out).toBe("你好世界")
|
||||
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 {
|
||||
Object.assign(config, saved)
|
||||
server.stop(true)
|
||||
|
||||
Reference in new issue
Block a user