- Electron + React + TypeScript 架构 - 三栏布局: Sidebar | ChatPanel | DetailPanel - 9 个内置工具 (文件系统/网络/记忆/命令) - SQLite 持久化 (better-sqlite3) - MUI 暗色/亮色主题系统 - Agent Loop ReAct 状态机引擎 - DeepSeek / Agnes AI / Ollama Provider 适配器 - MCP 协议集成 - 系统托盘 + 全局快捷键 - Tailwind CSS v4 + Tailwind Merge - 修复: Sidebar 缺失 TextField 导入导致黑屏
136 lines
3.9 KiB
TypeScript
136 lines
3.9 KiB
TypeScript
/**
|
|
* Agent Loop — LLM 输出解析器
|
|
*
|
|
* 使用 Zod Schema 定义 ReAct 输出格式。
|
|
* 优先使用 Structured Output / Tool Calling,降级为正则解析。
|
|
*
|
|
* @see docs/生产级通用 AI Agent 智能体桌面应用:完整设计与构建指南.html — 第四章
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
import type { Thought } from './types';
|
|
import type { MetonaToolCall } from '../types';
|
|
|
|
/** 思考内容 Schema */
|
|
export const ThoughtSchema = z.object({
|
|
type: z.literal('thought'),
|
|
content: z.string().describe('当前的推理思考过程'),
|
|
needs_tool: z.boolean().describe('是否需要调用工具'),
|
|
});
|
|
|
|
/** 工具调用 Schema */
|
|
export const ToolCallSchema = z.object({
|
|
type: z.literal('tool_call'),
|
|
tool_name: z.string().describe('要调用的工具名称'),
|
|
args: z.record(z.unknown()).describe('工具参数'),
|
|
reasoning: z.string().describe('为什么选择这个工具'),
|
|
});
|
|
|
|
/** 最终答案 Schema */
|
|
export const FinalAnswerSchema = z.object({
|
|
type: z.literal('final_answer'),
|
|
answer: z.string().describe('最终答案内容'),
|
|
confidence: z.number().min(0).max(1).describe('答案置信度'),
|
|
summary: z.string().describe('简要总结'),
|
|
});
|
|
|
|
/** ReAct 步骤联合类型 */
|
|
export const ReActStepSchema = z.discriminatedUnion('type', [
|
|
ThoughtSchema,
|
|
ToolCallSchema,
|
|
FinalAnswerSchema,
|
|
]);
|
|
|
|
/**
|
|
* LLM 输出解析器
|
|
*/
|
|
export function parseLLMOutput(rawText: string, iteration: number): {
|
|
thought?: Thought;
|
|
toolCalls?: MetonaToolCall[];
|
|
} {
|
|
// 尝试 JSON 解析(Structured Output 模式)
|
|
try {
|
|
const jsonStart = rawText.indexOf('{');
|
|
const jsonEnd = rawText.lastIndexOf('}');
|
|
if (jsonStart !== -1 && jsonEnd !== -1) {
|
|
const jsonStr = rawText.slice(jsonStart, jsonEnd + 1);
|
|
const parsed = JSON.parse(jsonStr);
|
|
const validated = ReActStepSchema.safeParse(parsed);
|
|
|
|
if (validated.success) {
|
|
const data = validated.data;
|
|
switch (data.type) {
|
|
case 'thought':
|
|
return {
|
|
thought: {
|
|
id: `thought-${iteration}`,
|
|
content: data.content,
|
|
timestamp: Date.now(),
|
|
iteration,
|
|
},
|
|
};
|
|
case 'tool_call':
|
|
return {
|
|
toolCalls: [{
|
|
id: `call-${iteration}-${Date.now()}`,
|
|
name: data.tool_name,
|
|
args: data.args as Record<string, unknown>,
|
|
timestamp: Date.now(),
|
|
iteration,
|
|
}],
|
|
};
|
|
case 'final_answer':
|
|
return {
|
|
thought: {
|
|
id: `answer-${iteration}`,
|
|
content: `[FINAL ANSWER]\n${data.answer}\n\nConfidence: ${data.confidence}\nSummary: ${data.summary}`,
|
|
timestamp: Date.now(),
|
|
iteration,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// JSON 解析失败,降级到正则提取
|
|
}
|
|
|
|
// 降级:正则提取
|
|
return parseWithRegex(rawText, iteration);
|
|
}
|
|
|
|
/** 正则降级解析 */
|
|
function parseWithRegex(text: string, iteration: number): {
|
|
thought?: Thought;
|
|
toolCalls?: MetonaToolCall[];
|
|
} {
|
|
const result: { thought?: Thought; toolCalls?: MetonaToolCall[] } = {};
|
|
|
|
const thoughtMatch = text.match(/Thought:\s*([\s\S]*?)(?=Action:|$)/i);
|
|
if (thoughtMatch) {
|
|
result.thought = {
|
|
id: `thought-${iteration}`,
|
|
content: thoughtMatch[1].trim(),
|
|
timestamp: Date.now(),
|
|
iteration,
|
|
};
|
|
}
|
|
|
|
const actionMatch = text.match(/Action:\s*(\w+)\s*\n\s*Action Input:\s*({[\s\S]*?})\s*(?=\n\n|Observation:|$)/i);
|
|
if (actionMatch) {
|
|
try {
|
|
result.toolCalls = [{
|
|
id: `call-${iteration}-${Date.now()}`,
|
|
name: actionMatch[1].trim(),
|
|
args: JSON.parse(actionMatch[2]),
|
|
timestamp: Date.now(),
|
|
iteration,
|
|
}];
|
|
} catch {
|
|
// 参数 JSON 解析失败
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|