fix: 修复搜索引擎/DuckDuckGo超时/web_fetch UA/Agent去重误杀,优化搜索fallback指导,版本号更新至5.0.1
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
全离线运行 · 数据本地存储 · 零外部依赖
|
全离线运行 · 数据本地存储 · 零外部依赖
|
||||||
|
|
||||||
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
||||||
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
[](https://gitee.com/thzxx/metona-ollama-desktop/releases)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://www.electronjs.org/)
|
[](https://www.electronjs.org/)
|
||||||
@@ -294,7 +294,7 @@ src/
|
|||||||
|
|
||||||
| 文件 | 类型 |
|
| 文件 | 类型 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `Metona Ollama Setup 5.0.0.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
| `Metona Ollama Setup 5.0.1.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
||||||
|
|
||||||
> ⚠️ 未签名版本,首次运行 Windows 可能弹出安全警告,点击「仍要运行」即可。
|
> ⚠️ 未签名版本,首次运行 Windows 可能弹出安全警告,点击「仍要运行」即可。
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -14,7 +14,7 @@
|
|||||||
# 1. 克隆并安装依赖
|
# 1. 克隆并安装依赖
|
||||||
git clone https://gitee.com/thzxx/metona-ollama-desktop.git
|
git clone https://gitee.com/thzxx/metona-ollama-desktop.git
|
||||||
cd metona-ollama-desktop
|
cd metona-ollama-desktop
|
||||||
git checkout metona-ollama-desktop-v5.0.0
|
git checkout metona-ollama-desktop-v5.0.1
|
||||||
npm config set registry https://registry.npmmirror.com
|
npm config set registry https://registry.npmmirror.com
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
@@ -176,6 +176,15 @@ electron-builder 会自动处理所有依赖下载(Electron、NSIS、winCodeSi
|
|||||||
|
|
||||||
## 构建日志
|
## 构建日志
|
||||||
|
|
||||||
|
### v5.0.1(2026-04-18) — metona-ollama-desktop 仓库
|
||||||
|
|
||||||
|
- 🔧 修复 DuckDuckGo 搜索超时过长(15s → 8s),添加 Google 作为第三搜索引擎兜底
|
||||||
|
- 🔧 修复 web_fetch User-Agent(MetonaOllama/1.0 → Chrome UA),增加超时至 30 秒
|
||||||
|
- 🔧 修复 Agent Loop 跨轮去重误杀失败工具的重试(仅对成功的调用去重,失败的允许重试)
|
||||||
|
- 🔧 优化 TOOL_USAGE_GUIDE:新增搜索 fallback 规则(fetch 失败时直接用搜索结果回答)、禁止抓取 SPA 页面、搜索查询进化指导
|
||||||
|
- 🔧 更新 AGENT_SYSTEM_PROMPT:工具数 25 → 34,新增 fetch 失败 fallback 规则
|
||||||
|
- 📝 版本号更新至 5.0.1(package.json / package-lock.json / index.html / README.md / CHANGELOG.md / BUILD.md / DEVELOPMENT.md)
|
||||||
|
|
||||||
### v5.0.0(2026-04-18) — metona-ollama-desktop 仓库
|
### v5.0.0(2026-04-18) — metona-ollama-desktop 仓库
|
||||||
|
|
||||||
- ✨ 新增 8 个浏览器控制工具(browser_open/screenshot/evaluate/extract/click/type/scroll/close)
|
- ✨ 新增 8 个浏览器控制工具(browser_open/screenshot/evaluate/extract/click/type/scroll/close)
|
||||||
|
|||||||
+28
-1
@@ -3,7 +3,7 @@
|
|||||||
## 分支结构
|
## 分支结构
|
||||||
|
|
||||||
**桌面版主线**(`master`):
|
**桌面版主线**(`master`):
|
||||||
- `metona-ollama-desktop-v1.1` → `metona-ollama-desktop-v2` → `metona-ollama-desktop-v3.0.0` → `metona-ollama-desktop-v3.1.0` → `metona-ollama-desktop-v3.1.1` → `metona-ollama-desktop-v3.2.0` → `metona-ollama-desktop-v3.2.1` → `metona-ollama-desktop-v3.2.2` → `metona-ollama-desktop-v3.2.3` → `metona-ollama-desktop-v3.2.4` → `metona-ollama-desktop-v3.2.5` → `metona-ollama-desktop-v3.2.6` → `metona-ollama-desktop-v3.3.0` → `metona-ollama-desktop-v3.3.1` → `metona-ollama-desktop-v3.3.2` → `metona-ollama-desktop-v3.3.3` → `metona-ollama-desktop-v3.3.4` → `metona-ollama-desktop-v4.0.0` → `metona-ollama-desktop-v5.0.0`(当前发布分支)
|
- `metona-ollama-desktop-v3.2.6` → `metona-ollama-desktop-v3.3.4` → `metona-ollama-desktop-v4.0.0` → `metona-ollama-desktop-v5.0.0` → `metona-ollama-desktop-v5.0.1`(当前发布分支)
|
||||||
|
|
||||||
**Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。
|
**Web 版历史**已独立为 [metona-ollama-web](https://gitee.com/thzxx/metona-ollama-web) 仓库(v1.1.0 ~ v4.0.3)。
|
||||||
|
|
||||||
@@ -11,6 +11,33 @@
|
|||||||
|
|
||||||
## 桌面版(Desktop)
|
## 桌面版(Desktop)
|
||||||
|
|
||||||
|
### Desktop v5.0.1 — 搜索引擎优化 & Agent 去重修复
|
||||||
|
|
||||||
|
- 🔧 **修复 DuckDuckGo 搜索超时过长**
|
||||||
|
- 超时从 15 秒降至 8 秒(国内环境 DuckDuckGo 通常不可达,减少等待)
|
||||||
|
- 添加 Google 作为第三搜索引擎兜底(DuckDuckGo → Bing → Google)
|
||||||
|
- 新增 `parseGoogleResults()` 解析函数
|
||||||
|
- 搜索 User-Agent 统一更新为 Chrome 131
|
||||||
|
- 🔧 **修复 web_fetch 无法抓取部分网站**
|
||||||
|
- User-Agent 从 `MetonaOllama/1.0` 改为 Chrome 131 UA(部分网站屏蔽非浏览器 UA)
|
||||||
|
- 超时从 15 秒增加至 30 秒(大页面加载更慢)
|
||||||
|
- 新增 `Accept` 和 `Accept-Language` 请求头
|
||||||
|
- 🔧 **修复 Agent Loop 跨轮去重误杀失败工具的重试**
|
||||||
|
- 原逻辑:连续两轮工具调用(工具名+参数)完全相同即终止
|
||||||
|
- 问题:上一轮 `web_fetch` 超时失败,下一轮同样调用是合理重试,却被当作重复终止
|
||||||
|
- 修复:仅对成功的工具调用做去重,失败的允许重试
|
||||||
|
- 新增 `prevLoopSuccessKeys` 替代 `prevLoopToolKeys`
|
||||||
|
- 🔧 **优化 TOOL_USAGE_GUIDE(新增 3 条规则)**
|
||||||
|
- 新增规则 2:web_fetch 失败时直接用 web_search 摘要回答,不要反复重试同一 URL
|
||||||
|
- 新增规则 3:禁止抓取 SPA 页面(GitHub releases、npm 等),直接从搜索结果提取信息
|
||||||
|
- 新增规则 4:搜索查询要进化,不要重复相同的查询
|
||||||
|
- 🔧 **更新 AGENT_SYSTEM_PROMPT**
|
||||||
|
- 工具数量描述从 25 → 34
|
||||||
|
- 核心规则新增:web_search 结果中的 snippet 已包含关键信息,不要忽视
|
||||||
|
- 核心规则细化:同一 URL 连续失败 2 次后不要再尝试
|
||||||
|
- 多工具协同示例新增:web_fetch 失败时直接用搜索结果回答
|
||||||
|
- 📝 **版本号更新至 5.0.1**(7 个文件同步更新)
|
||||||
|
|
||||||
### Desktop v5.0.0 — 浏览器控制 & MCP & 技能系统 & 全面重构
|
### Desktop v5.0.0 — 浏览器控制 & MCP & 技能系统 & 全面重构
|
||||||
|
|
||||||
- ✨ **MCP (Model Context Protocol) 完整实现**
|
- ✨ **MCP (Model Context Protocol) 完整实现**
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Metona Ollama Desktop — 开发规范
|
# Metona Ollama Desktop — 开发规范
|
||||||
|
|
||||||
> 版本: 5.0.0 | 更新: 2026-04-18 | 维护: 项目团队
|
> 版本: 5.0.1 | 更新: 2026-04-18 | 维护: 项目团队
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sql.js": "^1.11.0"
|
"sql.js": "^1.11.0"
|
||||||
@@ -4398,7 +4398,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/make-fetch-happen/node_modules/http-proxy-agent": {
|
"node_modules/make-fetch-happen/node_modules/http-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||||
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
|
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5791,7 +5791,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar/node_modules/minipass": {
|
"node_modules/tar/node_modules/minipass": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz",
|
||||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
"author": "thzxx",
|
"author": "thzxx",
|
||||||
|
|||||||
+68
-16
@@ -437,8 +437,12 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
|
|||||||
|
|
||||||
sendLog('info', `🌐 web_fetch`, `${url} (${maxChars > 0 ? maxChars + ' chars' : '完整内容'})`);
|
sendLog('info', `🌐 web_fetch`, `${url} (${maxChars > 0 ? maxChars + ' chars' : '完整内容'})`);
|
||||||
const resp = await fetch(url, {
|
const resp = await fetch(url, {
|
||||||
headers: { 'User-Agent': 'MetonaOllama/1.0' },
|
headers: {
|
||||||
signal: AbortSignal.timeout(15000)
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
@@ -499,18 +503,20 @@ export async function handleWebSearch(params: { query: string; max_results?: num
|
|||||||
|
|
||||||
sendLog('info', `🔍 web_search`, `"${query}" (${maxResults} 条)`);
|
sendLog('info', `🔍 web_search`, `"${query}" (${maxResults} 条)`);
|
||||||
|
|
||||||
// 1. 尝试 DuckDuckGo HTML 搜索
|
const searchHeaders = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'text/html',
|
||||||
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. 尝试 DuckDuckGo HTML 搜索(8 秒超时,国内环境通常不可用)
|
||||||
let results: Array<{ title: string; url: string; snippet: string }> = [];
|
let results: Array<{ title: string; url: string; snippet: string }> = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ddgUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
const ddgUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
||||||
const resp = await fetch(ddgUrl, {
|
const resp = await fetch(ddgUrl, {
|
||||||
headers: {
|
headers: searchHeaders,
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
signal: AbortSignal.timeout(8000)
|
||||||
'Accept': 'text/html',
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(15000)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@@ -518,19 +524,15 @@ export async function handleWebSearch(params: { query: string; max_results?: num
|
|||||||
results = parseDDGResults(html, maxResults);
|
results = parseDDGResults(html, maxResults);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sendLog('warn', `🔍 DuckDuckGo 搜索失败`, (e as Error).message);
|
sendLog('warn', `🔍 DuckDuckGo 搜索失败(8s 超时)`, (e as Error).message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 如果 DuckDuckGo 无结果,尝试备用方案
|
// 2. DuckDuckGo 无结果,尝试 Bing
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
try {
|
try {
|
||||||
const bingUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}`;
|
const bingUrl = `https://www.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}`;
|
||||||
const resp = await fetch(bingUrl, {
|
const resp = await fetch(bingUrl, {
|
||||||
headers: {
|
headers: searchHeaders,
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
||||||
'Accept': 'text/html',
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(15000)
|
signal: AbortSignal.timeout(15000)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -543,6 +545,24 @@ export async function handleWebSearch(params: { query: string; max_results?: num
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Bing 也失败,尝试 Google(通过 google.com/search)
|
||||||
|
if (results.length === 0) {
|
||||||
|
try {
|
||||||
|
const googleUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}&num=${maxResults}`;
|
||||||
|
const resp = await fetch(googleUrl, {
|
||||||
|
headers: searchHeaders,
|
||||||
|
signal: AbortSignal.timeout(15000)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resp.ok) {
|
||||||
|
const html = await resp.text();
|
||||||
|
results = parseGoogleResults(html, maxResults);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
sendLog('warn', `🔍 Google 搜索失败`, (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
return { success: false, error: '未找到搜索结果,请尝试其他关键词' };
|
return { success: false, error: '未找到搜索结果,请尝试其他关键词' };
|
||||||
}
|
}
|
||||||
@@ -621,6 +641,38 @@ function parseBingResults(html: string, maxResults: number): Array<{ title: stri
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 解析 Google HTML 搜索结果 */
|
||||||
|
function parseGoogleResults(html: string, maxResults: number): Array<{ title: string; url: string; snippet: string }> {
|
||||||
|
const results: Array<{ title: string; url: string; snippet: string }> = [];
|
||||||
|
|
||||||
|
// Google 结果块:<div class="g">...</div>,内含 <a href="..."><h3>title</h3></a> 和摘要
|
||||||
|
const blockRegex = /<div class="g"[^>]*>([\s\S]*?)(?=<div class="g"|<\/div>\s*<\/div>\s*<\/div>)/gi;
|
||||||
|
let blockMatch;
|
||||||
|
|
||||||
|
while ((blockMatch = blockRegex.exec(html)) !== null && results.length < maxResults) {
|
||||||
|
const block = blockMatch[1];
|
||||||
|
|
||||||
|
const titleMatch = block.match(/<a[^>]*href="(https?:\/\/[^"]*)"[^>]*>[\s\S]*?<h3[^>]*>([\s\S]*?)<\/h3>/);
|
||||||
|
if (!titleMatch) continue;
|
||||||
|
|
||||||
|
const url = titleMatch[1].trim();
|
||||||
|
const title = decodeHTML(titleMatch[2].replace(/<[^>]+>/g, '').trim());
|
||||||
|
|
||||||
|
// 摘要:尝试多种模式
|
||||||
|
const snippetMatch =
|
||||||
|
block.match(/<div[^>]*data-sncf="[^"]*"[^>]*>([\s\S]*?)<\/div>/) ||
|
||||||
|
block.match(/<span[^>]*class="[^"]*st[^"]*"[^>]*>([\s\S]*?)<\/span>/) ||
|
||||||
|
block.match(/<div[^>]*class="[^"]*VwiC3b[^"]*"[^>]*>([\s\S]*?)<\/div>/);
|
||||||
|
const snippet = snippetMatch ? decodeHTML(snippetMatch[1].replace(/<[^>]+>/g, '').trim()) : '';
|
||||||
|
|
||||||
|
if (title && url.startsWith('http') && !url.includes('google.com')) {
|
||||||
|
results.push({ title, url, snippet });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/** 简单 HTML 实体解码 */
|
/** 简单 HTML 实体解码 */
|
||||||
function decodeHTML(text: string): string {
|
function decodeHTML(text: string): string {
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="logo">🦙</span>
|
<span class="logo">🦙</span>
|
||||||
<span class="app-title">Metona Ollama</span>
|
<span class="app-title">Metona Ollama</span>
|
||||||
<span class="app-version">v5.0.0</span>
|
<span class="app-version">v5.0.1</span>
|
||||||
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||||
|
|||||||
@@ -37,22 +37,23 @@ const MAX_RETRIES = 2; // 工具错误自动重试次数
|
|||||||
/** v4.1: 工具并行执行 — 依赖检测用 */
|
/** v4.1: 工具并行执行 — 依赖检测用 */
|
||||||
const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch', 'edit_file', 'append_file', 'move_file', 'delete_file']);
|
const TOOLS_WITH_DATA_DEPS = new Set(['web_fetch', 'edit_file', 'append_file', 'move_file', 'delete_file']);
|
||||||
|
|
||||||
const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手。你有 25 个工具可以调用,包括文件操作、联网搜索、网页抓取、命令执行、记忆管理等。
|
const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手。你有 34 个工具可以调用,包括文件操作、联网搜索、网页抓取、命令执行、浏览器控制、记忆管理、MCP 工具等。
|
||||||
|
|
||||||
## 核心规则
|
## 核心规则
|
||||||
|
|
||||||
1. 直接调用工具,不要写"我来帮你搜索"然后结束。有工具就调用,调用完根据结果继续下一步。
|
1. 直接调用工具,不要写"我来帮你搜索"然后结束。有工具就调用,调用完根据结果继续下一步。
|
||||||
2. 多步任务必须逐步完成:搜到结果 → 抓取详情 → 给出答案。不要在中间步骤停下。
|
2. 多步任务必须逐步完成:搜到结果 → 给出答案。如果需要详情再抓取,但抓取失败时直接用搜索结果回答,不要卡在抓取上。
|
||||||
3. 需要最新信息(版本号、新闻、日期等)时,必须先用工具获取,不要凭记忆猜测。
|
3. 需要最新信息(版本号、新闻、日期等)时,必须先用工具获取,不要凭记忆猜测。
|
||||||
4. 工具出错时分析原因并换方法重试,最多重试 2 次。
|
4. 工具出错时分析原因并换方法重试,最多重试 2 次。同一 URL 连续失败 2 次后不要再尝试,换其他方式。
|
||||||
5. 不要重复调用相同参数的同一工具。
|
5. 不要重复调用相同参数的同一工具。
|
||||||
|
6. web_search 结果中的标题、URL、snippet 已经包含关键信息,不要忽视它们。
|
||||||
|
|
||||||
## 多工具协同
|
## 多工具协同
|
||||||
|
|
||||||
你可以在一轮回复中调用多个工具(并行调用),也可以在一个工具的结果基础上继续调用下一个工具(链式调用)。这是正常且鼓励的工作方式。
|
你可以在一轮回复中调用多个工具(并行调用),也可以在一个工具的结果基础上继续调用下一个工具(链式调用)。这是正常且鼓励的工作方式。
|
||||||
|
|
||||||
常见链式调用模式:
|
常见链式调用模式:
|
||||||
- web_search → web_fetch(搜索 → 抓取详情)
|
- web_search → web_fetch(搜索 → 抓取详情,但 web_fetch 失败时直接用搜索结果回答)
|
||||||
- list_directory → read_file(列出目录 → 读取文件)
|
- list_directory → read_file(列出目录 → 读取文件)
|
||||||
- search_files → edit_file(搜索 → 修改)
|
- search_files → edit_file(搜索 → 修改)
|
||||||
- run_command → read_file(执行命令 → 读取输出文件)
|
- run_command → read_file(执行命令 → 读取输出文件)
|
||||||
@@ -63,13 +64,16 @@ const AGENT_SYSTEM_PROMPT = `你是一个具备工具调用能力的 AI 助手
|
|||||||
|
|
||||||
const TOOL_USAGE_GUIDE = `【工具链规则(强制执行)】
|
const TOOL_USAGE_GUIDE = `【工具链规则(强制执行)】
|
||||||
|
|
||||||
1. 搜索后必须抓取:web_search 搜到结果后,必须选择最相关的 1-3 个 URL,用 web_fetch 抓取详细内容。只看搜索摘要就回答是不够的。这是强制规则。
|
1. 搜索后优先抓取:web_search 搜到结果后,选择最相关的 URL 用 web_fetch 抓取详情。
|
||||||
2. 先搜索再回答:需要最新信息时,先 search → 再 fetch → 再回答。不要凭记忆猜测版本号、日期、价格等实时数据。
|
2. 抓取失败时直接用搜索结果:如果 web_fetch 失败(超时、网络错误等),不要反复重试同一个 URL。直接利用 web_search 返回的摘要和标题回答用户问题。搜索结果的 snippet 已经包含了足够的信息。
|
||||||
3. 文件路径上下文:用户说"修改这个文件"等模糊指代时,从最近的工具调用结果中提取实际路径。不要猜测。
|
3. 不要抓取 SPA 页面:GitHub releases、npm 包页面等是 SPA(单页应用),纯 HTTP 抓取拿不到有效内容。直接从搜索结果中提取版本号等信息即可。
|
||||||
4. 不要重复调用:已经成功调用过的工具+参数组合不要再调用。
|
4. 搜索查询要进化:如果第一次搜索结果不理想,换更精确的关键词再搜,不要重复相同的查询。
|
||||||
5. 记忆工具:可以用 memory_search 搜索过去记忆,用 memory_add 添加重要信息。
|
5. 先搜索再回答:需要最新信息(版本号、新闻、日期等)时,先 search → 再回答。不要凭记忆猜测。
|
||||||
6. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。
|
6. 文件路径上下文:用户说"修改这个文件"等模糊指代时,从最近的工具调用结果中提取实际路径。不要猜测。
|
||||||
7. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。`;
|
7. 不要重复调用:已经成功调用过的工具+参数组合不要再调用。
|
||||||
|
8. 记忆工具:可以用 memory_search 搜索过去记忆,用 memory_add 添加重要信息。
|
||||||
|
9. 会话工具:可以用 session_list 列出历史会话,用 session_read 读取会话内容。
|
||||||
|
10. 并行调用:当多个工具调用互相独立时,可以在同一次回复中同时调用它们。`;
|
||||||
|
|
||||||
/** 工具名白名单:用于文本解析兜底时过滤非法工具名 */
|
/** 工具名白名单:用于文本解析兜底时过滤非法工具名 */
|
||||||
const VALID_TOOL_NAMES = new Set([
|
const VALID_TOOL_NAMES = new Set([
|
||||||
@@ -467,7 +471,8 @@ export async function runAgentLoop(
|
|||||||
const loopStartTime = Date.now();
|
const loopStartTime = Date.now();
|
||||||
let content = '';
|
let content = '';
|
||||||
let totalEvalCount = 0;
|
let totalEvalCount = 0;
|
||||||
let prevLoopToolKeys: string[] = [];
|
/** 跨轮去重:仅跟踪成功的工具调用(失败的允许重试) */
|
||||||
|
let prevLoopSuccessKeys: string[] = [];
|
||||||
/** 每轮工具调用计数(用于去重) */
|
/** 每轮工具调用计数(用于去重) */
|
||||||
let allCallsThisLoop: ToolCall[] = [];
|
let allCallsThisLoop: ToolCall[] = [];
|
||||||
|
|
||||||
@@ -645,16 +650,22 @@ export async function runAgentLoop(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跨轮次完整重复检测
|
// 跨轮次重复检测:仅当连续两轮调用完全相同且全部成功时才终止(失败的允许重试)
|
||||||
const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort();
|
const currentLoopKeys = toolCalls.map(c => getToolCacheKey(c.function.name, c.function.arguments)).sort();
|
||||||
const currentKeysStr = JSON.stringify(currentLoopKeys);
|
const currentKeysStr = JSON.stringify(currentLoopKeys);
|
||||||
const prevKeysStr = JSON.stringify([...prevLoopToolKeys].sort());
|
const prevKeysStr = JSON.stringify([...prevLoopSuccessKeys].sort());
|
||||||
if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) {
|
if (currentKeysStr === prevKeysStr && currentLoopKeys.length > 0) {
|
||||||
logWarn('检测到连续两轮工具调用完全相同,终止 ReAct Loop', currentKeysStr.slice(0, 200));
|
// 检查是否有上一轮失败的工具——如果有,不终止,允许重试
|
||||||
callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats());
|
const hasFailedInPrev = allToolRecords
|
||||||
return;
|
.filter(r => prevLoopSuccessKeys.includes(getToolCacheKey(r.name, r.arguments)))
|
||||||
|
.some(r => r.status !== 'success');
|
||||||
|
if (!hasFailedInPrev) {
|
||||||
|
logWarn('检测到连续两轮工具调用完全相同且全部成功,终止 ReAct Loop', currentKeysStr.slice(0, 200));
|
||||||
|
callbacks.onDone(content || '(检测到重复工具调用,已自动停止)', allToolRecords, makeStats());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logInfo('检测到重复调用但上一轮有失败,允许重试');
|
||||||
}
|
}
|
||||||
prevLoopToolKeys = currentLoopKeys;
|
|
||||||
|
|
||||||
// 记录 ReAct 轨迹
|
// 记录 ReAct 轨迹
|
||||||
const traceStep = {
|
const traceStep = {
|
||||||
@@ -811,6 +822,11 @@ export async function runAgentLoop(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新跨轮去重:仅跟踪本轮成功的工具调用
|
||||||
|
prevLoopSuccessKeys = allToolRecords
|
||||||
|
.filter(r => r.status === 'success')
|
||||||
|
.map(r => getToolCacheKey(r.name, r.arguments));
|
||||||
|
|
||||||
// 保存本轮轨迹
|
// 保存本轮轨迹
|
||||||
traceStep.observation = allToolRecords.slice(-toolCalls.length).map(r =>
|
traceStep.observation = allToolRecords.slice(-toolCalls.length).map(r =>
|
||||||
`${r.name}: ${r.result?.success ? 'success' : 'error'}`
|
`${r.name}: ${r.result?.success ? 'success' : 'error'}`
|
||||||
|
|||||||
Reference in New Issue
Block a user