接入文章优化LLM追踪
This commit is contained in:
@@ -205,18 +205,17 @@ describe("job API routes", () => {
|
||||
});
|
||||
|
||||
it("streams a one-click optimization from body-only input", async () => {
|
||||
llmMocks.generateValidatedJson
|
||||
.mockResolvedValueOnce(validCandidateFactCard)
|
||||
.mockResolvedValueOnce({
|
||||
title: "流式优化标题",
|
||||
summary: "流式优化摘要。",
|
||||
body_markdown:
|
||||
"## 服务能力\nExample Technology Co., Ltd. 提供 GEO optimization 服务。",
|
||||
image_suggestions: [],
|
||||
changed_sections: ["title", "body"],
|
||||
requires_user_confirmation: [],
|
||||
})
|
||||
.mockResolvedValueOnce({ checks: [] });
|
||||
mockTrackedLlmResult(validCandidateFactCard, "llmcall_fact");
|
||||
mockTrackedLlmResult({
|
||||
title: "流式优化标题",
|
||||
summary: "流式优化摘要。",
|
||||
body_markdown:
|
||||
"## 服务能力\nExample Technology Co., Ltd. 提供 GEO optimization 服务。",
|
||||
image_suggestions: [],
|
||||
changed_sections: ["title", "body"],
|
||||
requires_user_confirmation: [],
|
||||
}, "llmcall_draft");
|
||||
mockTrackedLlmResult({ checks: [] }, "llmcall_qa");
|
||||
|
||||
const response = await optimizeStream(
|
||||
request({
|
||||
@@ -231,13 +230,25 @@ describe("job API routes", () => {
|
||||
expect(response.status).toBe(200);
|
||||
expect(events.map((event) => event.type)).toEqual([
|
||||
"job_created",
|
||||
"llm_call_started",
|
||||
"llm_call_responded",
|
||||
"llm_call_validated",
|
||||
"fact_card_ready",
|
||||
"draft_started",
|
||||
"llm_call_started",
|
||||
"llm_call_responded",
|
||||
"llm_call_validated",
|
||||
"draft_ready",
|
||||
"qa_started",
|
||||
"llm_call_started",
|
||||
"llm_call_responded",
|
||||
"llm_call_validated",
|
||||
"qa_ready",
|
||||
"final_ready",
|
||||
]);
|
||||
expect(JSON.stringify(events)).not.toContain("messages");
|
||||
expect(JSON.stringify(events)).not.toContain("choices");
|
||||
expect(JSON.stringify(events)).not.toContain("test-key");
|
||||
expect(
|
||||
events.find((event) => event.type === "fact_card_ready")?.fact_card,
|
||||
).toEqual(expect.objectContaining({ confirmed_by_user: false }));
|
||||
@@ -784,6 +795,47 @@ async function createJobFixture() {
|
||||
return response.json() as Promise<{ job: { id: string } }>;
|
||||
}
|
||||
|
||||
function mockTrackedLlmResult(value: unknown, callId: string) {
|
||||
llmMocks.generateValidatedJson.mockImplementationOnce(async (input) => {
|
||||
const onTraceEvent = input.onTraceEvent as
|
||||
| ((event: Record<string, unknown>) => void | Promise<void>)
|
||||
| undefined;
|
||||
await onTraceEvent?.({
|
||||
type: "started",
|
||||
call_id: callId,
|
||||
task: input.task ?? "unknown",
|
||||
context: {
|
||||
workflow_stage: input.traceStage ?? "unknown",
|
||||
rewrite_round: input.rewriteRound,
|
||||
schema_name: input.schemaName,
|
||||
},
|
||||
provider: "deepseek",
|
||||
model: "deepseek-v4-pro",
|
||||
request: { model: "deepseek-v4-pro", messages: [] },
|
||||
started_at: "2026-07-16T00:00:00.000Z",
|
||||
});
|
||||
await onTraceEvent?.({
|
||||
type: "responded",
|
||||
call_id: callId,
|
||||
response: {
|
||||
choices: [{ message: { content: JSON.stringify(value) } }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 4, total_tokens: 14 },
|
||||
},
|
||||
duration_ms: 1200,
|
||||
responded_at: "2026-07-16T00:00:01.200Z",
|
||||
});
|
||||
await onTraceEvent?.({
|
||||
type: "validated",
|
||||
call_id: callId,
|
||||
schema_name: input.schemaName ?? "anonymousSchema",
|
||||
schema_valid: true,
|
||||
validation_issues: [],
|
||||
validated_at: "2026-07-16T00:00:01.300Z",
|
||||
});
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function request(body: unknown, options: { apiKey?: string | null } = {}) {
|
||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||
const apiKey = options.apiKey === undefined ? "test-key" : options.apiKey;
|
||||
|
||||
@@ -7,6 +7,13 @@ import { getRepositoryFromRuntime } from "../../../../lib/db/repository";
|
||||
import { optimizationFactCardSchema } from "../../../../lib/domain/validation";
|
||||
import type { LlmAuditSummary } from "../../../../lib/llm/audit";
|
||||
import { LlmValidationError } from "../../../../lib/llm/client";
|
||||
import { getLlmTracePayloadStoreFromRuntime } from "../../../../lib/llm/trace-payload-store";
|
||||
import {
|
||||
createLlmTraceRecorder,
|
||||
createNoopLlmTraceRecorder,
|
||||
safeTraceError,
|
||||
} from "../../../../lib/llm/trace-recorder";
|
||||
import { getLlmTraceRepositoryFromRuntime } from "../../../../lib/llm/trace-repository";
|
||||
import { getExportStoreFromRuntime } from "../../../../lib/workflow/export-store";
|
||||
import { extractCandidateFactCard } from "../../../../lib/workflow/fact-extractor";
|
||||
import {
|
||||
@@ -61,6 +68,7 @@ export async function POST(request: Request) {
|
||||
let jobId: string | undefined;
|
||||
let caseId: string | undefined;
|
||||
let stage: OptimizationStreamStage = "job";
|
||||
let traceRecorder = createNoopLlmTraceRecorder();
|
||||
const llmAuditSummary: LlmAuditSummary[] = [];
|
||||
const processSummary: ProcessSummaryStep[] = [];
|
||||
const requestStartedAt = Date.now();
|
||||
@@ -104,6 +112,23 @@ export async function POST(request: Request) {
|
||||
case: { id: optimizationCase.id, case_type: "article" },
|
||||
});
|
||||
|
||||
try {
|
||||
traceRecorder = await createLlmTraceRecorder({
|
||||
jobId: job.id,
|
||||
caseId: optimizationCase.id,
|
||||
repository: getLlmTraceRepositoryFromRuntime(),
|
||||
payloadStore: getLlmTracePayloadStoreFromRuntime(),
|
||||
publish: (event) => send(event),
|
||||
});
|
||||
} catch (error) {
|
||||
send({
|
||||
type: "trace_warning",
|
||||
job_id: job.id,
|
||||
trace_completeness: "incomplete",
|
||||
error_summary: safeTraceError(error),
|
||||
});
|
||||
}
|
||||
|
||||
stage = "fact_card";
|
||||
const factCardStartedAt = Date.now();
|
||||
const factCard = optimizationFactCardSchema.parse(
|
||||
@@ -112,6 +137,7 @@ export async function POST(request: Request) {
|
||||
onAuditSummary: (summary) => {
|
||||
llmAuditSummary.push(summary);
|
||||
},
|
||||
onTraceEvent: traceRecorder.onLlmEvent,
|
||||
})),
|
||||
);
|
||||
processSummary.push(
|
||||
@@ -124,20 +150,24 @@ export async function POST(request: Request) {
|
||||
}),
|
||||
);
|
||||
const savedFactCard = await repository.saveFactCard(job.id, factCard);
|
||||
send({
|
||||
const factCardReadyEvent: OptimizationStreamEvent = {
|
||||
type: "fact_card_ready",
|
||||
job_id: job.id,
|
||||
fact_card: savedFactCard,
|
||||
});
|
||||
};
|
||||
await traceRecorder.onWorkflowEvent(factCardReadyEvent);
|
||||
send(factCardReadyEvent);
|
||||
|
||||
const result = await runStreamingOptimizationWorkflow({
|
||||
jobId: job.id,
|
||||
input: normalized.articleInput,
|
||||
factCard: savedFactCard,
|
||||
onEvent: (event) => {
|
||||
onEvent: async (event) => {
|
||||
stage = stageForEvent(event, stage);
|
||||
await traceRecorder.onWorkflowEvent(event);
|
||||
send(event);
|
||||
},
|
||||
onTraceEvent: traceRecorder.onLlmEvent,
|
||||
onAuditSummary: (summary) => {
|
||||
llmAuditSummary.push(summary);
|
||||
},
|
||||
@@ -180,7 +210,7 @@ export async function POST(request: Request) {
|
||||
error_stage: null,
|
||||
error_summary: null,
|
||||
});
|
||||
send({
|
||||
const finalReadyEvent: OptimizationStreamEvent = {
|
||||
type: "final_ready",
|
||||
job_id: job.id,
|
||||
case: { id: optimizationCase.id, case_type: "article" },
|
||||
@@ -188,7 +218,10 @@ export async function POST(request: Request) {
|
||||
optimized_article: optimizedArticle,
|
||||
qa_report: qaReport,
|
||||
export_paths: exportPaths,
|
||||
});
|
||||
};
|
||||
await traceRecorder.onWorkflowEvent(finalReadyEvent);
|
||||
await traceRecorder.finish({ status: "completed" });
|
||||
send(finalReadyEvent);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "优化失败";
|
||||
let failedVersion:
|
||||
@@ -225,14 +258,21 @@ export async function POST(request: Request) {
|
||||
failedVersion = undefined;
|
||||
}
|
||||
}
|
||||
send({
|
||||
const failedEvent: OptimizationStreamEvent = {
|
||||
type: "failed",
|
||||
job_id: jobId,
|
||||
case: caseId ? { id: caseId, case_type: "article" } : undefined,
|
||||
result_version: failedVersion,
|
||||
stage,
|
||||
error: message,
|
||||
};
|
||||
await traceRecorder.onWorkflowEvent(failedEvent);
|
||||
await traceRecorder.finish({
|
||||
status: "failed",
|
||||
errorStage: stage,
|
||||
errorSummary: message,
|
||||
});
|
||||
send(failedEvent);
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
LlmTraceCallStatus,
|
||||
LlmTraceCompleteness,
|
||||
LlmTraceErrorType,
|
||||
LlmTraceRun,
|
||||
LlmTraceRunStatus,
|
||||
LlmTraceWorkflowStage,
|
||||
} from "./trace-types";
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
LlmTraceCallStatus,
|
||||
LlmTraceCompleteness,
|
||||
LlmTraceErrorType,
|
||||
LlmTraceRun,
|
||||
LlmTraceRunStatus,
|
||||
LlmTraceWorkflowStage,
|
||||
} from "./trace-types";
|
||||
|
||||
@@ -13,12 +13,14 @@ import {
|
||||
export interface OptimizeArticleInput {
|
||||
input: ArticleInput;
|
||||
factCard: OptimizationFactCard;
|
||||
onTraceEvent?: GenerateInput["onTraceEvent"];
|
||||
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||
}
|
||||
|
||||
export async function optimizeArticle({
|
||||
input,
|
||||
factCard,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
}: OptimizeArticleInput): Promise<OptimizedArticle> {
|
||||
const llmArticle = await generateValidatedJson({
|
||||
@@ -27,6 +29,9 @@ export async function optimizeArticle({
|
||||
prompt: buildArticleOptimizerPrompt(input, factCard),
|
||||
temperature: 0.2,
|
||||
task: "article_optimizer",
|
||||
schemaName: "optimizedArticleSchema",
|
||||
traceStage: "draft",
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
|
||||
export async function extractCandidateFactCard(
|
||||
input: ArticleInput,
|
||||
options: { onAuditSummary?: GenerateInput["onAuditSummary"] } = {},
|
||||
options: {
|
||||
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||
onTraceEvent?: GenerateInput["onTraceEvent"];
|
||||
} = {},
|
||||
): Promise<CandidateFactCard> {
|
||||
return generateValidatedJson({
|
||||
schema: candidateFactCardSchema,
|
||||
@@ -16,6 +19,9 @@ export async function extractCandidateFactCard(
|
||||
prompt: buildFactExtractorPrompt(input),
|
||||
temperature: 0.1,
|
||||
task: "fact_extractor",
|
||||
schemaName: "candidateFactCardSchema",
|
||||
traceStage: "fact_card",
|
||||
onTraceEvent: options.onTraceEvent,
|
||||
onAuditSummary: options.onAuditSummary,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ export interface InspectQualityInput {
|
||||
factCard: OptimizationFactCard;
|
||||
platform: PublishPlatform;
|
||||
sourceImages: ImageInput[];
|
||||
rewriteRound?: number;
|
||||
onTraceEvent?: GenerateInput["onTraceEvent"];
|
||||
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||
}
|
||||
|
||||
@@ -72,6 +74,10 @@ export async function inspectQualityWithLlm(
|
||||
}),
|
||||
temperature: 0.1,
|
||||
task: "quality_inspector",
|
||||
schemaName: "llmQaPatchSchema",
|
||||
traceStage: "qa",
|
||||
rewriteRound: input.rewriteRound,
|
||||
onTraceEvent: input.onTraceEvent,
|
||||
onAuditSummary: input.onAuditSummary,
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface RunStreamingOptimizationWorkflowInput {
|
||||
input: ArticleInput;
|
||||
factCard: OptimizationFactCard;
|
||||
onEvent: (event: OptimizationStreamEvent) => void | Promise<void>;
|
||||
onTraceEvent?: GenerateInput["onTraceEvent"];
|
||||
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@ export async function runStreamingOptimizationWorkflow({
|
||||
input,
|
||||
factCard,
|
||||
onEvent,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
}: RunStreamingOptimizationWorkflowInput) {
|
||||
const processSummary: ProcessSummaryStep[] = [];
|
||||
@@ -31,7 +33,12 @@ export async function runStreamingOptimizationWorkflow({
|
||||
message: "正在生成优化草稿",
|
||||
});
|
||||
let stageStartedAt = Date.now();
|
||||
let article = await optimizeArticle({ input, factCard, onAuditSummary });
|
||||
let article = await optimizeArticle({
|
||||
input,
|
||||
factCard,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
});
|
||||
processSummary.push(
|
||||
createProcessStep({
|
||||
stage: "draft",
|
||||
@@ -54,6 +61,8 @@ export async function runStreamingOptimizationWorkflow({
|
||||
factCard,
|
||||
platform: input.platform,
|
||||
sourceImages: input.images,
|
||||
rewriteRound: 0,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
});
|
||||
processSummary.push(
|
||||
@@ -81,6 +90,8 @@ export async function runStreamingOptimizationWorkflow({
|
||||
article,
|
||||
factCard,
|
||||
failedChecks,
|
||||
rewriteRound: nextRound,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
});
|
||||
rewriteRounds = nextRound;
|
||||
@@ -112,6 +123,8 @@ export async function runStreamingOptimizationWorkflow({
|
||||
factCard,
|
||||
platform: input.platform,
|
||||
sourceImages: input.images,
|
||||
rewriteRound: nextRound,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
});
|
||||
processSummary.push(
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface RewriteFailedSectionsInput {
|
||||
article: OptimizedArticle;
|
||||
factCard: OptimizationFactCard;
|
||||
failedChecks: QaCheck[];
|
||||
rewriteRound?: number;
|
||||
onTraceEvent?: GenerateInput["onTraceEvent"];
|
||||
onAuditSummary?: GenerateInput["onAuditSummary"];
|
||||
}
|
||||
|
||||
@@ -17,6 +19,8 @@ export async function rewriteFailedSections({
|
||||
article,
|
||||
factCard,
|
||||
failedChecks,
|
||||
rewriteRound,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
}: RewriteFailedSectionsInput): Promise<OptimizedArticle> {
|
||||
const llmArticle = await generateValidatedJson({
|
||||
@@ -25,6 +29,10 @@ export async function rewriteFailedSections({
|
||||
prompt: buildTargetedRewritePrompt({ article, factCard, failedChecks }),
|
||||
temperature: 0.15,
|
||||
task: "targeted_rewriter",
|
||||
schemaName: "optimizedArticleSchema",
|
||||
traceStage: "rewrite",
|
||||
rewriteRound,
|
||||
onTraceEvent,
|
||||
onAuditSummary,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user