优化标题生成功能
Agent CI/CD / docker-image (push) Successful in 21s
Agent CI/CD / deploy-fallback-log (push) Has been skipped

This commit is contained in:
2026-05-22 14:20:27 +08:00
parent ab12d79d91
commit 4c47841483
3 changed files with 70 additions and 6 deletions
+1
View File
@@ -564,6 +564,7 @@ export const buildChatRouter = (
if (shouldGenerateTitle) { if (shouldGenerateTitle) {
sessionTitle = await generateSessionTitle(runtime, { sessionTitle = await generateSessionTitle(runtime, {
sessionId: binding.sessionId, sessionId: binding.sessionId,
latestAssistantMessage: assistantText,
latestUserMessage: parsed.data.message, latestUserMessage: parsed.data.message,
fallbackTitle: existingSessionTitle, fallbackTitle: existingSessionTitle,
}); });
+33 -5
View File
@@ -12,15 +12,29 @@ const TITLE_CONTEXT_MESSAGE_CHAR_LIMIT = 240;
const RESTORE_TURN_LIMIT = 8; const RESTORE_TURN_LIMIT = 8;
const RESTORE_MESSAGE_CHAR_LIMIT = 480; const RESTORE_MESSAGE_CHAR_LIMIT = 480;
const RESTORE_CONTEXT_CHAR_LIMIT = 3200; const RESTORE_CONTEXT_CHAR_LIMIT = 3200;
const DEFAULT_SESSION_TITLE = "新对话";
const buildSessionTitle = (message: string) => { const buildSessionTitle = (message: string) => {
const normalized = message.replace(/\s+/g, " ").trim(); const normalized = message.replace(/\s+/g, " ").trim();
if (!normalized) { if (!normalized) {
return "新对话"; return DEFAULT_SESSION_TITLE;
} }
return normalized.length > 24 ? `${normalized.slice(0, 24)}...` : normalized; return normalized.length > 24 ? `${normalized.slice(0, 24)}...` : normalized;
}; };
const appendTitleContextMessage = (
lines: string[],
role: "用户" | "助手",
content: string | undefined,
maxLength = TITLE_CONTEXT_MESSAGE_CHAR_LIMIT,
) => {
const normalized = content?.replace(/\s+/g, " ").trim();
if (!normalized) {
return;
}
lines.push(`${role}${normalized.slice(0, maxLength)}`);
};
const buildTitleConversationContext = async ( const buildTitleConversationContext = async (
runtime: OpencodeRuntimeAdapter, runtime: OpencodeRuntimeAdapter,
sessionId: string, sessionId: string,
@@ -67,7 +81,9 @@ const buildTitleConversationContext = async (
const normalizeGeneratedTitle = (rawTitle: string, fallback: string) => { const normalizeGeneratedTitle = (rawTitle: string, fallback: string) => {
const normalized = rawTitle const normalized = rawTitle
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.replace(/^标题[:]\s*/i, "")
.replace(/["'“”‘’`]/g, "") .replace(/["'“”‘’`]/g, "")
.replace(/[。!?!?,,、;;:]+$/g, "")
.trim(); .trim();
if (!normalized) { if (!normalized) {
return fallback; return fallback;
@@ -85,13 +101,24 @@ export const generateSessionTitle = async (
options: { options: {
sessionId: string; sessionId: string;
latestUserMessage: string; latestUserMessage: string;
latestAssistantMessage?: string;
fallbackTitle?: string; fallbackTitle?: string;
}, },
) => { ) => {
const fallback = options.fallbackTitle?.trim() || buildSessionTitle(options.latestUserMessage); const fallbackTitle = options.fallbackTitle?.trim();
const fallback =
fallbackTitle && fallbackTitle !== DEFAULT_SESSION_TITLE
? fallbackTitle
: buildSessionTitle(options.latestUserMessage);
let titleSessionId: string | undefined; let titleSessionId: string | undefined;
try { try {
const conversation = await buildTitleConversationContext(runtime, options.sessionId); const scopedContext: string[] = [];
appendTitleContextMessage(scopedContext, "用户", options.latestUserMessage, 480);
appendTitleContextMessage(scopedContext, "助手", options.latestAssistantMessage, 960);
const conversation =
scopedContext.length > 0
? scopedContext.join("\n")
: await buildTitleConversationContext(runtime, options.sessionId);
if (!conversation) { if (!conversation) {
return fallback; return fallback;
} }
@@ -104,8 +131,9 @@ export const generateSessionTitle = async (
[ [
"你是会话标题生成器。", "你是会话标题生成器。",
"请根据下面整段多轮对话生成一个 8-16 字中文标题。", "请根据下面整段多轮对话生成一个 8-16 字中文标题。",
"要求:简洁、可读、避免标点、不要引号、不要解释。", "要求:简洁、具体、可读、避免标点、不要引号、不要解释。",
"先理解完整对话,再概括核心任务或结论。", "优先概括用户当前真实需求和助手最终结论。",
"忽略系统提示、历史记忆、学习上下文、工具日志等元信息。",
"不要直接照抄用户任一条消息原文。", "不要直接照抄用户任一条消息原文。",
"只输出标题本身。", "只输出标题本身。",
"", "",
+36 -1
View File
@@ -1,6 +1,10 @@
import { describe, expect, it } from "bun:test"; import { describe, expect, it } from "bun:test";
import { shouldGenerateSessionTitle } from "../../src/routes/chatSession.js"; import {
generateSessionTitle,
shouldGenerateSessionTitle,
} from "../../src/routes/chatSession.js";
import { type OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
describe("shouldGenerateSessionTitle", () => { describe("shouldGenerateSessionTitle", () => {
it("allows auto-title generation for the first turn when the title was not edited", () => { it("allows auto-title generation for the first turn when the title was not edited", () => {
@@ -36,3 +40,34 @@ describe("shouldGenerateSessionTitle", () => {
).toBe(false); ).toBe(false);
}); });
}); });
describe("generateSessionTitle", () => {
it("uses the current user and assistant turn instead of reading wrapped runtime context", async () => {
let titlePrompt = "";
const runtime = {
createSession: async () => ({ id: "title-session" }),
prompt: async (_sessionId: string, prompt: string) => {
titlePrompt = prompt;
},
waitForSessionIdle: async () => undefined,
messages: async () => [
{
info: { role: "assistant" },
parts: [{ type: "text", text: "标题:泵站压力异常排查。" }],
},
],
abortSession: async () => undefined,
} as unknown as OpencodeRuntimeAdapter;
const title = await generateSessionTitle(runtime, {
sessionId: "chat-session",
latestUserMessage: "检查一下三号泵站最近压力波动的原因",
latestAssistantMessage: "三号泵站压力波动主要与夜间阀门开度变化有关。",
fallbackTitle: "新对话",
});
expect(title).toBe("泵站压力异常排查");
expect(titlePrompt).toContain("用户:检查一下三号泵站最近压力波动的原因");
expect(titlePrompt).toContain("助手:三号泵站压力波动主要与夜间阀门开度变化有关。");
});
});