From 27d7a44ad0c58575d3c0d72b811fa06646fd4668 Mon Sep 17 00:00:00 2001 From: czj <13261895355@163.com> Date: Thu, 2 Jul 2026 14:34:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=BA=E5=91=B3=E5=84=BF?= =?UTF-8?q?=E6=96=87=E6=A1=88=E4=BC=98=E5=8C=96=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/llm/__tests__/prompts.test.ts | 37 +++++++ src/lib/llm/client.ts | 3 +- src/lib/llm/prompts.ts | 101 ++++++++++++++++++ .../__tests__/llm-integration.test.ts | 40 +++++++ src/lib/workflow/renwei-copy-optimizer.ts | 22 ++++ 5 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/lib/workflow/renwei-copy-optimizer.ts diff --git a/src/lib/llm/__tests__/prompts.test.ts b/src/lib/llm/__tests__/prompts.test.ts index ee3fb8a..e25b446 100644 --- a/src/lib/llm/__tests__/prompts.test.ts +++ b/src/lib/llm/__tests__/prompts.test.ts @@ -4,10 +4,12 @@ import { ARTICLE_OPTIMIZER_SYSTEM_PROMPT, FACT_EXTRACTOR_SYSTEM_PROMPT, QUALITY_INSPECTOR_SYSTEM_PROMPT, + RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT, TARGETED_REWRITER_SYSTEM_PROMPT, buildArticleOptimizerPrompt, buildFactExtractorPrompt, buildQualityInspectorPrompt, + buildRenweiCopyOptimizationPrompt, buildTargetedRewritePrompt, } from "../prompts"; @@ -168,4 +170,39 @@ describe("LLM prompt builders", () => { expect(prompt).toContain("body_quality"); expect(prompt).toContain("不得新增事实"); }); + + it("renwei copy prompt keeps the author present and defaults to small edits", () => { + const prompt = `${RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT}\n${buildRenweiCopyOptimizationPrompt({ + source_text: "我观察到大家越来越难进入心流了。", + goal: "保留原意,减少 AI 味", + intensity: "light", + user_instructions: "保留作者的口语感。", + })}`; + + expect(prompt).toContain("少动"); + expect(prompt).toContain("保留作者"); + expect(prompt).toContain("手迹"); + expect(prompt).toContain("不凭空新增时间、地点、数字、案例"); + expect(prompt).toContain("不写过度金句"); + expect(prompt).toContain("逐处说明"); + expect(prompt).toContain("只检查被改动句子"); + expect(prompt).toContain("source_text"); + }); + + it("renwei copy prompt describes all result fields and intensity modes", () => { + const prompt = buildRenweiCopyOptimizationPrompt({ + source_text: "这是一段文案。", + goal: "更自然", + intensity: "conversational", + user_instructions: "", + }); + + expect(prompt).toContain("optimized_text"); + expect(prompt).toContain("change_notes"); + expect(prompt).toContain("ai_taste_checks"); + expect(prompt).toContain("warnings"); + expect(prompt).toContain("light"); + expect(prompt).toContain("medium"); + expect(prompt).toContain("conversational"); + }); }); diff --git a/src/lib/llm/client.ts b/src/lib/llm/client.ts index 60c133f..07bd9fb 100644 --- a/src/lib/llm/client.ts +++ b/src/lib/llm/client.ts @@ -6,7 +6,8 @@ export type LlmTaskName = | "fact_extractor" | "article_optimizer" | "quality_inspector" - | "targeted_rewriter"; + | "targeted_rewriter" + | "renwei_copy_optimizer"; export interface GenerateInput { system?: string; diff --git a/src/lib/llm/prompts.ts b/src/lib/llm/prompts.ts index 9612aa9..ef5948a 100644 --- a/src/lib/llm/prompts.ts +++ b/src/lib/llm/prompts.ts @@ -1,5 +1,6 @@ import type { ArticleInput, + CopyOptimizationRequest, OptimizationFactCard, OptimizedArticle, PublishPlatform, @@ -97,6 +98,44 @@ const QA_OUTPUT_CONTRACT = [ ), ].join("\n"); +const RENWEI_OUTPUT_CONTRACT = [ + "Output type contract:", + "- optimized_text must be a string.", + "- change_notes must be an array of { original, revised, reason, confidence, revertible }.", + "- confidence must be confident or uncertain.", + "- revertible must be boolean.", + "- ai_taste_checks must be an array of { rule_id, status, evidence, suggestion }.", + "- rule_id must be one of meaning_inflation, promotion_tone, formulaic_sentence, format_trace, chat_trace, filler_hedging.", + "- status must be pass or warn.", + "- warnings must be string[].", + "Example:", + JSON.stringify( + { + optimized_text: "我把句子顺了一下。", + change_notes: [ + { + original: "我把句子顺顺。", + revised: "我把句子顺了一下。", + reason: "修掉重复,让句子自然收住。", + confidence: "confident", + revertible: false, + }, + ], + ai_taste_checks: [ + { + rule_id: "promotion_tone", + status: "pass", + evidence: "没有新增宣传腔。", + suggestion: "", + }, + ], + warnings: [], + }, + null, + 2, + ), +].join("\n"); + function formatPlatformGuidance(selected: PublishPlatform) { return [ "平台模板选项:", @@ -135,6 +174,29 @@ export const TARGETED_REWRITER_SYSTEM_PROMPT = [ JSON_ONLY_PROMPT, ].join(" "); +export const RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT = [ + "你是中文普通文案编辑,不是营销代笔工具。", + "目标是让文字更顺,但改完后作者还在。", + "硬性规则:少动,只改真正打绊的地方;保留作者的位置、语气、口头习惯和可识别手迹。", + "不凭空新增时间、地点、数字、案例、情绪、场景、资质、效果承诺或没有来源的事实。", + "不写过度金句,不要把普通句子改成宣传腔、排比、格言、万能展望或意义拔高。", + "拿不准时白描,不拔高;拿不准的改动必须标记为 uncertain 且 revertible 为 true。", + "改后只检查被改动句子是否引入 AI 味,并逐处说明改了什么、为什么改。", + JSON_ONLY_PROMPT, +].join(" "); + +function describeRenweiIntensity( + intensity: CopyOptimizationRequest["intensity"], +) { + const descriptions = { + light: "light / 轻微整理:尽量只修顺病句、错别字、明显卡顿。", + medium: "medium / 适度润色:允许调整句序和连接,但不改变作者表达的粗糙感。", + conversational: + "conversational / 更口语自然:让口吻更像真人说话,但不凭空增加表演性口语。", + } satisfies Record; + return descriptions[intensity]; +} + export function buildFactExtractorPrompt(input: ArticleInput) { return [ "Return a CandidateFactCard JSON object with these exact keys:", @@ -247,3 +309,42 @@ export function buildTargetedRewritePrompt(input: { JSON.stringify(input.failedChecks, null, 2), ].join("\n"); } + +export function buildRenweiCopyOptimizationPrompt( + input: CopyOptimizationRequest, +) { + return [ + "Return a CopyOptimizationResult JSON object with these exact keys:", + "optimized_text, change_notes, ai_taste_checks, warnings.", + "", + RENWEI_OUTPUT_CONTRACT, + "", + "强度选项:", + "- light:轻微整理。", + "- medium:适度润色。", + "- conversational:更口语自然。", + `当前修改强度:${describeRenweiIntensity(input.intensity)}`, + "", + "改稿要求:", + "- 默认少动,不追求更漂亮,只清掉绊脚处。", + "- 毛边先假设是作者手迹,不要机械删口语、停顿或重复。", + "- 不写“不是X,而是Y”、排比三连、万能展望、宣传腔或假深刻句式。", + "- 不使用破折号作为装饰性解释。", + "- 必须逐处说明改动;未改动的句子不要编造改动说明。", + "- AI 味检查只检查被改动句子。", + "", + "User request:", + JSON.stringify( + { + goal: input.goal, + intensity: input.intensity, + user_instructions: input.user_instructions, + }, + null, + 2, + ), + "", + "source_text:", + input.source_text, + ].join("\n"); +} diff --git a/src/lib/workflow/__tests__/llm-integration.test.ts b/src/lib/workflow/__tests__/llm-integration.test.ts index 6f60c16..ee53efb 100644 --- a/src/lib/workflow/__tests__/llm-integration.test.ts +++ b/src/lib/workflow/__tests__/llm-integration.test.ts @@ -17,6 +17,7 @@ vi.mock("../../llm/client", async () => { import { extractCandidateFactCard } from "../fact-extractor"; import { optimizeArticle } from "../article-optimizer"; import { inspectQualityWithLlm } from "../quality-inspector"; +import { optimizeRenweiCopy } from "../renwei-copy-optimizer"; import { rewriteFailedSections } from "../targeted-rewriter"; describe("LLM workflow integration", () => { @@ -356,4 +357,43 @@ describe("LLM workflow integration", () => { expect(hallucinationCheck?.status).toBe("warn"); expect(report.overall_status).toBe("warn"); }); + + it("uses validated LLM output for renwei copy optimization", async () => { + llmMocks.generateValidatedJson.mockResolvedValueOnce({ + optimized_text: "我观察到大家越来越难进入心流了。", + change_notes: [ + { + original: "我观察到大家越来越难进入心流", + revised: "我观察到大家越来越难进入心流了。", + reason: "补足句尾语气,让句子自然收住。", + confidence: "confident", + revertible: false, + }, + ], + ai_taste_checks: [ + { + rule_id: "promotion_tone", + status: "pass", + evidence: "没有新增宣传腔。", + suggestion: "", + }, + ], + warnings: [], + }); + + const result = await optimizeRenweiCopy({ + source_text: "我观察到大家越来越难进入心流", + goal: "保留原意,减少 AI 味", + intensity: "light", + user_instructions: "", + }); + + expect(result.optimized_text).toBe("我观察到大家越来越难进入心流了。"); + expect(llmMocks.generateValidatedJson).toHaveBeenCalledWith( + expect.objectContaining({ + task: "renwei_copy_optimizer", + temperature: 0.2, + }), + ); + }); }); diff --git a/src/lib/workflow/renwei-copy-optimizer.ts b/src/lib/workflow/renwei-copy-optimizer.ts new file mode 100644 index 0000000..a5d6be0 --- /dev/null +++ b/src/lib/workflow/renwei-copy-optimizer.ts @@ -0,0 +1,22 @@ +import type { + CopyOptimizationRequest, + CopyOptimizationResult, +} from "../domain/types"; +import { copyOptimizationResultSchema } from "../domain/validation"; +import { generateValidatedJson } from "../llm/client"; +import { + RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT, + buildRenweiCopyOptimizationPrompt, +} from "../llm/prompts"; + +export async function optimizeRenweiCopy( + input: CopyOptimizationRequest, +): Promise { + return generateValidatedJson({ + schema: copyOptimizationResultSchema, + system: RENWEI_COPY_OPTIMIZER_SYSTEM_PROMPT, + prompt: buildRenweiCopyOptimizationPrompt(input), + temperature: 0.2, + task: "renwei_copy_optimizer", + }); +}