haixunMaster/lib/ai/fact-check.ts

175 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { z } from "zod";
import { withAgentSystem } from "./agent";
import type { ProviderApiKeys } from "./keys";
import { generateStructuredObject } from "./generate-structured";
import { getModel } from "./provider";
import {
getProviderLabel,
isBraveSearchConfigured,
searchWebThorough,
type SearchProvider,
} from "@/lib/services/web-search";
const classifySchema = z.object({
isKnowledgeContent: z.boolean(),
reason: z.string().optional().default(""),
claims: z.array(z.string()).max(5).default([]),
});
const verdictSchema = z.object({
passed: z.boolean(),
summary: z.string(),
issues: z.array(z.string()),
verifiedPoints: z.array(z.string()),
sources: z.array(
z.object({
title: z.string(),
link: z.string(),
})
),
});
export interface FactCheckResult {
isKnowledgeContent: boolean;
passed: boolean;
summary: string;
issues: string[];
verifiedPoints: string[];
sources: Array<{ title: string; link: string }>;
searchProvider: SearchProvider | "none";
searchProviderLabel: string;
skipped?: boolean;
skipReason?: string;
}
export async function factCheckDraft(params: {
text: string;
angle?: string | null;
searchTag?: string | null;
topicLabel?: string | null;
aiProvider: string;
aiModel: string;
apiKeys?: ProviderApiKeys;
}): Promise<FactCheckResult> {
const model = getModel(params.aiProvider, params.aiModel, params.apiKeys ?? {});
const classified = await generateStructuredObject({
model,
provider: params.aiProvider,
modelId: params.aiModel,
schema: classifySchema,
system: withAgentSystem(
"判斷 Threads 貼文是否屬於「知識型」內容(含健康、醫療、營養、數據、科學、法規、步驟教學等可被查證的陳述)。"
),
prompt: `貼文:
${params.text}
${params.angle ? `角度:${params.angle}` : ""}
${params.searchTag ? `標籤:${params.searchTag}` : ""}
${params.topicLabel ? `主題:${params.topicLabel}` : ""}
若主要是語錄、心情、故事、觀點抒發且無具體可驗證事實 → isKnowledgeContent=false。
若是知識型,列出 15 個需要網路搜尋查證的關鍵陳述claims`,
});
if (!classified.isKnowledgeContent) {
return {
isKnowledgeContent: false,
passed: true,
summary: classified.reason || "非知識型內容,無需查證即可發布。",
issues: [],
verifiedPoints: [],
sources: [],
searchProvider: "none",
searchProviderLabel: "略過",
skipped: true,
skipReason: classified.reason,
};
}
const claims = classified.claims.filter(Boolean).slice(0, 5);
if (claims.length === 0) {
return {
isKnowledgeContent: true,
passed: false,
summary: "判定為知識型內容,但無法提取可查證陳述,請改寫得更具體或人工確認後再發。",
issues: ["無法提取可查證的關鍵陳述"],
verifiedPoints: [],
sources: [],
searchProvider: "none",
searchProviderLabel: "無",
};
}
const searchBlocks: string[] = [];
let primaryProvider: SearchProvider = "brave";
let providerLabel = isBraveSearchConfigured() ? "Brave Search" : "未設定";
const allResults: Array<{ title: string; link: string; snippet: string }> = [];
for (const claim of claims) {
const query = `${claim} ${params.topicLabel ?? ""}`.trim();
const { results, provider, providerLabel: label } = await searchWebThorough(query, 5, {
patrolMode: false,
threadsOnly: false,
});
primaryProvider = provider;
providerLabel = label;
for (const r of results) {
allResults.push({ title: r.title, link: r.link, snippet: r.snippet });
searchBlocks.push(
`【查詢:${query}】(${getProviderLabel(r.provider)}\n- ${r.title}\n ${r.snippet}\n ${r.link}`
);
}
}
if (allResults.length === 0) {
return {
isKnowledgeContent: true,
passed: false,
summary:
"網路搜尋暫無結果,知識型內容暫不建議發布。請稍後重試或改寫成較好查證的表述。",
issues: ["查無可參考來源(請設定 BRAVE_SEARCH_API_KEY 或稍後重試)"],
verifiedPoints: [],
sources: [],
searchProvider: primaryProvider,
searchProviderLabel: providerLabel,
};
}
const verdict = await generateStructuredObject({
model,
provider: params.aiProvider,
modelId: params.aiModel,
schema: verdictSchema,
system: withAgentSystem(`你是事實查核編輯。知識型 Threads 貼文發布前,必須根據網路搜尋結果判斷是否可發。
規則:
- 只有搜尋結果能支持或合理支持的核心陳述才算通過
- 醫療健康:保守判斷,有疑慮就 passed=false
- issues 列出具體哪句話有問題、為什麼
- verifiedPoints 列出已獲搜尋結果支持的陳述
- sources 從搜尋結果選 25 個最相關連結
- 繁體中文台灣用語`),
prompt: `待發布貼文:
${params.text}
需查證陳述:
${claims.map((c, i) => `${i + 1}. ${c}`).join("\n")}
網路搜尋結果(來源:${providerLabel}
${searchBlocks.join("\n\n")}
請判斷是否可發布。`,
});
return {
isKnowledgeContent: true,
passed: verdict.passed,
summary: verdict.summary,
issues: verdict.issues,
verifiedPoints: verdict.verifiedPoints,
sources: verdict.sources,
searchProvider: primaryProvider,
searchProviderLabel: providerLabel,
};
}