fix(dictionary): 修复 AI 编排系统的错误处理和超时控制

- 修复 orchestrator 中 throw 字符串的问题,改为 throw LookUpError
- 为 zhipu.ts 添加 30 秒超时控制,防止 LLM 调用卡死
- stage1 添加 isEmpty 和 isNaturalLanguage 字段验证
- stage2 改为降级处理而非直接失败,提升用户体验
- types.ts 添加 canMap 字段
- AGENTS.md 添加禁止擅自运行 pnpm dev 的说明
This commit is contained in:
2026-03-09 17:19:12 +08:00
parent 6ba5ae993a
commit 3652e350e6
6 changed files with 33 additions and 6 deletions

View File

@@ -1,7 +1,7 @@
# LEARN-LANGUAGES 知识库 # LEARN-LANGUAGES 知识库
**生成时间:** 2026-03-08 **生成时间:** 2026-03-08
**提交:** 91c59c3 **提交:** 6ba5ae9
**分支:** dev **分支:** dev
## 概述 ## 概述
@@ -112,6 +112,7 @@ log.error("Failed to fetch folders", { error });
- ❌ Server Component 可行时用 Client Component - ❌ Server Component 可行时用 Client Component
- ❌ npm 或 yarn (使用 pnpm) - ❌ npm 或 yarn (使用 pnpm)
- ❌ 生产代码中使用 `console.log` (使用 winston logger) - ❌ 生产代码中使用 `console.log` (使用 winston logger)
- ❌ 擅自运行 `pnpm dev` (不需要,用 `pnpm build` 验证即可)
## 独特风格 ## 独特风格

View File

@@ -53,7 +53,7 @@ export async function executeDictionaryLookup(
if (!standardFormResult.standardForm) { if (!standardFormResult.standardForm) {
log.error("[Stage 3] Standard form is empty"); log.error("[Stage 3] Standard form is empty");
throw "无法生成标准形式"; throw new LookUpError("无法生成标准形式");
} }
log.debug("[Stage 3] Standard form complete", { standardFormResult }); log.debug("[Stage 3] Standard form complete", { standardFormResult });

View File

@@ -55,6 +55,14 @@ export async function analyzeInput(text: string): Promise<InputAnalysisResult> {
throw new Error("阶段1isValid 字段类型错误"); throw new Error("阶段1isValid 字段类型错误");
} }
if (typeof result.isEmpty !== "boolean") {
throw new Error("阶段1isEmpty 字段类型错误");
}
if (typeof result.isNaturalLanguage !== "boolean") {
throw new Error("阶段1isNaturalLanguage 字段类型错误");
}
// 确保 reason 字段存在 // 确保 reason 字段存在
if (typeof result.reason !== "string") { if (typeof result.reason !== "string") {
result.reason = ""; result.reason = "";

View File

@@ -9,7 +9,7 @@ const log = createLogger("dictionary-stage2");
* 阶段 2跨语言语义映射决策 * 阶段 2跨语言语义映射决策
* *
* 独立的 LLM 调用,决定是否需要语义映射 * 独立的 LLM 调用,决定是否需要语义映射
* 如果输入不符合"明确、基础、可词典化的语义概念"且语言不一致,直接返回失败 * 如果输入不符合"明确、基础、可词典化的语义概念"且语言不一致,则降级使用原始输入
*/ */
export async function determineSemanticMapping( export async function determineSemanticMapping(
@@ -86,9 +86,17 @@ b) 输入是明确、基础、可词典化的语义概念
result.reason = ""; result.reason = "";
} }
// 如果不应该映射,返回错误 // 如果不应该映射,返回降级结果(不抛出错误
// 这样可以让后续阶段使用原始输入继续处理
if (!result.shouldMap) { if (!result.shouldMap) {
throw new Error(result.reason || "输入不符合可词典化的语义概念,无法进行跨语言查询"); log.debug("Semantic mapping not applicable, using original input", {
reason: result.reason
});
return {
shouldMap: false,
canMap: result.canMap ?? false,
reason: result.reason,
};
} }
if (!result.mappedQuery || result.mappedQuery.trim().length === 0) { if (!result.mappedQuery || result.mappedQuery.trim().length === 0) {
@@ -97,6 +105,7 @@ b) 输入是明确、基础、可词典化的语义概念
return { return {
shouldMap: result.shouldMap, shouldMap: result.shouldMap,
canMap: result.canMap ?? true,
coreSemantic: result.coreSemantic, coreSemantic: result.coreSemantic,
mappedQuery: result.mappedQuery, mappedQuery: result.mappedQuery,
reason: result.reason, reason: result.reason,

View File

@@ -20,6 +20,7 @@ export interface InputAnalysisResult {
// 阶段2语义映射结果 // 阶段2语义映射结果
export interface SemanticMappingResult { export interface SemanticMappingResult {
shouldMap: boolean; shouldMap: boolean;
canMap?: boolean;
coreSemantic?: string; coreSemantic?: string;
mappedQuery?: string; mappedQuery?: string;
reason: string; reason: string;

View File

@@ -2,10 +2,15 @@
type Messages = { role: string; content: string; }[]; type Messages = { role: string; content: string; }[];
const LLM_TIMEOUT_MS = 30000;
async function callZhipuAPI( async function callZhipuAPI(
messages: Messages, messages: Messages,
model = process.env.ZHIPU_MODEL_NAME, model = process.env.ZHIPU_MODEL_NAME,
) { ) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
const response = await fetch(url, { const response = await fetch(url, {
@@ -22,8 +27,11 @@ async function callZhipuAPI(
type: "disabled", type: "disabled",
}, },
}), }),
signal: controller.signal,
}); });
clearTimeout(timeoutId);
if (!response.ok) { if (!response.ok) {
throw new Error(`API 调用失败: ${response.status} ${response.statusText}`); throw new Error(`API 调用失败: ${response.status} ${response.statusText}`);
} }