/** * 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, 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; }