feat: Agent Engine 文本解析兜底 — 支持不支持 function calling 的模型
当模型没有通过 Ollama API 的 tool_calls 字段返回工具调用,
而是在回复文本中写出 Action/Action Input 时(如 gemma4 系列),
自动从文本中提取工具调用,确保 ReAct Loop 正常执行。
支持的文本格式:
- **Action:** tool_name **Action Input:** {json}
- Action: tool_name Action Input: {json}
- 工具名白名单校验 + JSON 解析容错(单引号、尾逗号等)
This commit is contained in:
@@ -87,6 +87,89 @@ const TOOL_USAGE_GUIDE = `【工具使用规则】
|
||||
9. 记忆工具:你可以使用 memory_search 搜索过去的记忆,使用 memory_add 添加新的重要记忆。
|
||||
10. 会话工具:你可以使用 session_list 列出历史会话,使用 session_read 读取历史会话内容。`;
|
||||
|
||||
/** 工具名白名单:用于文本解析兜底时过滤非法工具名 */
|
||||
const VALID_TOOL_NAMES = new Set([
|
||||
'read_file', 'write_file', 'list_directory', 'search_files', 'create_directory',
|
||||
'delete_file', 'run_command', 'move_file', 'copy_file', 'web_fetch', 'web_search',
|
||||
'append_file', 'edit_file', 'get_file_info', 'tree', 'download_file', 'diff_files',
|
||||
'replace_in_files', 'read_multiple_files', 'git', 'compress',
|
||||
'memory_search', 'memory_add', 'session_list', 'session_read'
|
||||
]);
|
||||
|
||||
/**
|
||||
* 文本解析兜底:当模型没有通过 tool_calls 字段返回工具调用,
|
||||
* 而是在文本中写了 "Action: xxx" / "Action Input: {...}" 时,
|
||||
* 从文本中提取工具调用。
|
||||
*/
|
||||
function parseToolCallsFromText(content: string): ToolCall[] {
|
||||
const calls: ToolCall[] = [];
|
||||
|
||||
// 匹配模式1: **Action:** tool_name **Action Input:** {json}
|
||||
// 匹配模式2: Action: tool_name Action Input: {json}
|
||||
// 同时支持 Action 和 Action Input 写在同一段或多行
|
||||
const actionRegex = /\*?\*?Action:?\*?\*?\s*([\w]+)\s*[\r\n]+\s*\*?\*?Action\s*Input:?\*?\*?\s*(\{[\s\S]*?\})/gi;
|
||||
|
||||
let match;
|
||||
while ((match = actionRegex.exec(content)) !== null) {
|
||||
const toolName = match[1].trim();
|
||||
const argsStr = match[2].trim();
|
||||
|
||||
if (!VALID_TOOL_NAMES.has(toolName)) continue;
|
||||
|
||||
try {
|
||||
// 清理 JSON:去除可能的 markdown 代码块包裹
|
||||
let cleaned = argsStr.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
|
||||
const args = JSON.parse(cleaned);
|
||||
calls.push({
|
||||
type: 'function',
|
||||
function: { name: toolName, arguments: args }
|
||||
});
|
||||
} catch {
|
||||
// JSON 解析失败,尝试修复常见问题
|
||||
try {
|
||||
// 处理单引号、尾逗号等
|
||||
let fixed = argsStr
|
||||
.replace(/'/g, '"')
|
||||
.replace(/,\s*}/g, '}')
|
||||
.replace(/,\s*]/g, ']')
|
||||
.replace(/```json?\s*/g, '')
|
||||
.replace(/```/g, '')
|
||||
.trim();
|
||||
const args = JSON.parse(fixed);
|
||||
calls.push({
|
||||
type: 'function',
|
||||
function: { name: toolName, arguments: args }
|
||||
});
|
||||
} catch {
|
||||
logWarn(`文本解析兜底: 工具 ${toolName} 的参数 JSON 解析失败`, argsStr.slice(0, 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 备选模式:Action 和 Action Input 写在一行或用不同分隔
|
||||
if (calls.length === 0) {
|
||||
const singleLineRegex = /Action:\s*(\w+)\s*.*?Action\s*Input:\s*(\{[^}]+\})/gi;
|
||||
while ((match = singleLineRegex.exec(content)) !== null) {
|
||||
const toolName = match[1].trim();
|
||||
const argsStr = match[2].trim();
|
||||
if (!VALID_TOOL_NAMES.has(toolName)) continue;
|
||||
try {
|
||||
const args = JSON.parse(argsStr);
|
||||
calls.push({
|
||||
type: 'function',
|
||||
function: { name: toolName, arguments: args }
|
||||
});
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
}
|
||||
|
||||
if (calls.length > 0) {
|
||||
logInfo(`文本解析兜底: 从回复中提取到 ${calls.length} 个工具调用`, calls.map(c => c.function.name).join(', '));
|
||||
}
|
||||
|
||||
return calls;
|
||||
}
|
||||
|
||||
/** 工具调用缓存:key = toolName + JSON(args),用于避免重复调用 */
|
||||
const toolResultCache = new Map<string, ToolResult>();
|
||||
|
||||
@@ -340,6 +423,14 @@ export async function runAgentLoop(
|
||||
|
||||
logModelResponse(content.length, toolCalls.length);
|
||||
|
||||
// 文本解析兜底:模型没通过 tool_calls 返回但文本中写了 Action
|
||||
if (toolCalls.length === 0 && useTools) {
|
||||
const parsedCalls = parseToolCallsFromText(content);
|
||||
if (parsedCalls.length > 0) {
|
||||
toolCalls.push(...parsedCalls);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是 Final Answer(没有工具调用且包含 Final Answer)
|
||||
const isFinalAnswer = content.includes('Final Answer:') || content.includes('最终答案:');
|
||||
if (toolCalls.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user