fix: harden AI analysis provider responses

This commit is contained in:
2026-07-05 03:40:17 -07:00
parent 6ed7508983
commit ae561a7d45
3 changed files with 26 additions and 15 deletions

View File

@@ -30,6 +30,7 @@ function createChatModel(config: { baseUrl: string; apiKey: string; chatModel: s
return new ChatOpenAI({ return new ChatOpenAI({
apiKey: config.apiKey, apiKey: config.apiKey,
model: config.chatModel, model: config.chatModel,
maxTokens: 1800,
temperature: 0.2, temperature: 0.2,
configuration, configuration,
}); });
@@ -241,6 +242,7 @@ export async function generateElderAiAnalysis(
"输出 JSON 字段overallRiskLevel(low|medium|high|critical|unknown), summary, keyFindings, recommendations, dataGaps, citations, confidence, modelSummary。", "输出 JSON 字段overallRiskLevel(low|medium|high|critical|unknown), summary, keyFindings, recommendations, dataGaps, citations, confidence, modelSummary。",
"keyFindings[] 字段category, severity(info|warning|critical), evidence, citationIds。", "keyFindings[] 字段category, severity(info|warning|critical), evidence, citationIds。",
"recommendations[] 字段title, priority(low|normal|high|urgent), rationale, suggestedNextStep, citationIds。", "recommendations[] 字段title, priority(low|normal|high|urgent), rationale, suggestedNextStep, citationIds。",
"citations[] 只返回你实际引用过的 citation 对象citationIds 必须来自可用 citations。",
knowledgeUnavailableReason ? `知识库检索不可用:${knowledgeUnavailableReason}。请在 dataGaps 中包含“知识库检索不可用,分析未使用内部知识库”。` : "", knowledgeUnavailableReason ? `知识库检索不可用:${knowledgeUnavailableReason}。请在 dataGaps 中包含“知识库检索不可用,分析未使用内部知识库”。` : "",
`modelSummary 固定为 {"provider":"openai-compatible","chatModel":"${runtimeConfig.config.chatModel}","embeddingModel":"${runtimeConfig.config.embeddingModel}"}。`, `modelSummary 固定为 {"provider":"openai-compatible","chatModel":"${runtimeConfig.config.chatModel}","embeddingModel":"${runtimeConfig.config.embeddingModel}"}。`,
`可用 citations${JSON.stringify(citations)}`, `可用 citations${JSON.stringify(citations)}`,
@@ -250,7 +252,9 @@ export async function generateElderAiAnalysis(
let rawOutput: unknown; let rawOutput: unknown;
try { try {
const response = await model.invoke(prompt); const response = await model.invoke(prompt, {
response_format: { type: "json_object" },
});
const content = Array.isArray(response.content) const content = Array.isArray(response.content)
? response.content.map((item) => (typeof item === "string" ? item : "")).join("\n") ? response.content.map((item) => (typeof item === "string" ? item : "")).join("\n")
: String(response.content); : String(response.content);

View File

@@ -125,8 +125,12 @@ async function buildKnowledgeChunks(input: KnowledgeEntryInput): Promise<Service
} }
const chunks = chunkKnowledgeText(input); const chunks = chunkKnowledgeText(input);
const vectors = await embeddingsResult.embeddings.embedDocuments(chunks); try {
return { success: true, data: { chunks, vectors } }; const vectors = await embeddingsResult.embeddings.embedDocuments(chunks);
return { success: true, data: { chunks, vectors } };
} catch {
return { success: false, reason: "知识库向量生成失败", status: 500 };
}
} }
export async function listKnowledgeEntries(context: AuthContext): Promise<KnowledgeEntry[]> { export async function listKnowledgeEntries(context: AuthContext): Promise<KnowledgeEntry[]> {
@@ -214,15 +218,15 @@ async function updateEntryWithChunks(
const rows = await transaction const rows = await transaction
.update(aiKnowledgeEntries) .update(aiKnowledgeEntries)
.set({ .set({
scope: input.scope, scope: input.scope,
organizationId, organizationId,
title: input.title, title: input.title,
category: input.category, category: input.category,
tags: input.tags, tags: input.tags,
body: input.body, body: input.body,
status: input.status, status: input.status,
updatedByAccountId: context.account.id, updatedByAccountId: context.account.id,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(aiKnowledgeEntries.id, id)) .where(eq(aiKnowledgeEntries.id, id))
.returning(); .returning();
@@ -322,7 +326,8 @@ export async function retrieveKnowledge(
return { success: false, reason: embeddingsResult.reason, status: 500 }; return { success: false, reason: embeddingsResult.reason, status: 500 };
} }
const [embedding] = await embeddingsResult.embeddings.embedDocuments([query]); const vectors = await embeddingsResult.embeddings.embedDocuments([query]).catch(() => []);
const embedding = vectors[0];
if (!embedding) { if (!embedding) {
return { success: false, reason: "知识库向量生成失败", status: 500 }; return { success: false, reason: "知识库向量生成失败", status: 500 };
} }

View File

@@ -256,6 +256,7 @@ export function validateElderAiAnalysisOutput(value: unknown): ElderAiAnalysisOu
if (!isRecord(value)) { if (!isRecord(value)) {
return null; return null;
} }
const confidenceValue = typeof value.confidence === "string" ? Number(value.confidence) : value.confidence;
const keyFindings = Array.isArray(value.keyFindings) ? value.keyFindings.map(validateFinding) : null; const keyFindings = Array.isArray(value.keyFindings) ? value.keyFindings.map(validateFinding) : null;
const recommendations = Array.isArray(value.recommendations) ? value.recommendations.map(validateRecommendation) : null; const recommendations = Array.isArray(value.recommendations) ? value.recommendations.map(validateRecommendation) : null;
const citations = Array.isArray(value.citations) ? value.citations.map(validateCitation) : null; const citations = Array.isArray(value.citations) ? value.citations.map(validateCitation) : null;
@@ -265,7 +266,8 @@ export function validateElderAiAnalysisOutput(value: unknown): ElderAiAnalysisOu
if ( if (
!ELDER_AI_RISK_LEVELS.includes(value.overallRiskLevel as ElderAiRiskLevel) || !ELDER_AI_RISK_LEVELS.includes(value.overallRiskLevel as ElderAiRiskLevel) ||
typeof value.summary !== "string" || typeof value.summary !== "string" ||
typeof value.confidence !== "number" || typeof confidenceValue !== "number" ||
!Number.isFinite(confidenceValue) ||
!keyFindings || !keyFindings ||
keyFindings.includes(null) || keyFindings.includes(null) ||
!recommendations || !recommendations ||
@@ -285,7 +287,7 @@ export function validateElderAiAnalysisOutput(value: unknown): ElderAiAnalysisOu
recommendations: recommendations.filter((item): item is ElderAiRecommendation => Boolean(item)), recommendations: recommendations.filter((item): item is ElderAiRecommendation => Boolean(item)),
dataGaps, dataGaps,
citations: citations.filter((item): item is AiCitation => Boolean(item)), citations: citations.filter((item): item is AiCitation => Boolean(item)),
confidence: Math.max(0, Math.min(1, value.confidence)), confidence: Math.max(0, Math.min(1, confidenceValue)),
modelSummary, modelSummary,
}; };
} }