fix: harden AI analysis provider responses
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user