一、什么是搜索代理
在聊搜索代理之前,就需要先搞清楚代理是什么,简单理解可以是:LLM 在自主循环中使用工具
🌟 那么搜索代理的简单定义:LLM 使用各种搜索或检索工具,动态地,按需地获取相关上下文
我在测试各种模型对于检索工具的调用的时候,发现模型能力差异导致的搜索结果不同,优秀的模型的搜索路径规划的更加合理,并且搜索的方向非常准确,而能力较差的模型,方向飘忽不定,结果也很一般
在文章的最后我会附上测试日志:deepseek-菜谱 share.log
所以我发现影响搜索代理成功的因素之一是:更智能的模型,可以使搜索代理自主应对复杂问题并从错误中恢复
二、搜索代理和 RAG 的区别
Excaildraw 文件:https://gcntfv628ebr.feishu.cn/file/OoMPbYlVKoTxNmxZGhZcNt2znWe
.0O_Ybf8P.png)
搜索代理和 RAG 的最主要的区别在于工作执行流程:
- RAG 是预推理检索:系统预先从向量数据库检索出来结果之后输入给 LLM,这个时候 LLM 是被动的接受
- 搜索代理是即时检索:LLM 是主动决策者,由 LLM 来决定搜索什么,怎么搜索,并且可以根据中间结果来主动的调整搜索路径
搜索代理和 RAG 还有一个在优化行为方式的区别:
`glob`- 文件夹的结构: 例如:
tests/test_agent.ts这个就表示测试模块中的测试 agent 功能的文件的隐性含义 - 命名约定:例如:
*.config.js表示配置文件 、README.md表示文档说明 - 文件大小:可以隐含复杂性的背景信息
- 时间戳:可以表示文件是最近修改的,隐含该文件相关性比较强(因为这个文件是最近修改的,那用户输入的问题很大概率是因为他刚刚修改这个文件产生的,我们可以仔细想想自己使用 ClaudeCode 或者 Cursor 的 Agent 模式的时候是不是这样的呢)
在这种构建方式下,代理可以逐层构建理解,仅在工作记忆中保留必要信息,这种自我管理的上下文窗口使代理专注于相关子集,而不是被大量但可能无关的信息淹没。
****- 在大量数据时,RAG 的检索会比搜索代理快很多
- RAG 的开发构建策略是平稳的,有成熟的构建搜索方案:分叉索引、父文档索引等,但是搜索代理要有效的话,需要有深思熟虑的工程实践,要有成熟的指导开发方案,不如搜索代理会很容易进入死循环和无效检索来浪费上下文,在这个方向上,搜索代理的开发难度比 RAG 大多了
- 工作执行流程的区别,RAG 是预推理检索,搜索代理是即时检索
- 优化行为方式的区别,搜索代理的优化方向更多,策略是可以动态调整的,RAG 需要重新构建文档块
- 开发和维护成本的区别:搜索代理的维护成本比较低,文件系统工具即时开发,不需要像 RAG 那样整理文档块,然后构建向量数据库等,并且搜索代理调整和维护比较容易
- 大量数据的检索速度区别:在大量数据的前提下,RAG 检索速度回比搜索代理快很多
- 有效搜索代理开发难度大:要想搜索代理在实际的场景中有效,需要有合适的策略指导
- 概念级匹配:按代码“做什么”来查找,而不只是按名称,RAG 向量的语义搜索能够理解代码背后的含义。
📝 Agent 同时使用 grep 和语义搜索。Grep 擅长查找 精确模式,语义搜索擅长查找概念相似性。二者结合可获得最佳效果。
三、搜索代理的应用场景
1、 对于上下文信息密度有较高的要求:例如编程开发领域,开发者使用代码来实现自己严密的业务逻辑,这种场景下使用代理搜索会非常合适
倘若我们采用 RAG 的方式来检索,我们需要对代码库进行文档块的分割,就不能像文本那样简单的按照行数来分割了,因为代码上下文之间有很强的逻辑依赖,如果文档块中少一行 if 判断,表达的意思就是天差地别了,比较合适的的方法是结合语法树和代码依赖关联来进行分割,
所以这种情况下,
********四、具体实现
Excalidraw 文件:https://gcntfv628ebr.feishu.cn/file/OoMPbYlVKoTxNmxZGhZcNt2znWe
.DEfc0WbW.png)
我是使用 Ts 来实现的,我比较熟悉这个语言,我的实现比较简单,没有做过多的兼容,只保证自己本地运行成功,在实现的过程中我发现一些复杂的地方:
- 一个成熟的执行工具需要兼容多种系统的版本,这个时候是比较复杂的
- 工具的描述和名称要定义的合理,表达的清晰,不然模型会混乱调用
4.1、工具实现
4.1.1、GrepTool 工具实现
实现 GrepTool 工具对象时,需要一些工具的参数定义,比较重要的是工具参数
- pattern:搜索的正则表达式
- path:搜索路径(目录)
- include:文件模式过滤
import { InternalTool, InternalToolContext } from '../types.js';
import { ripGrep } from '../../utils/ripgrep.js';
export const TOOL_NAME_FOR_PROMPT = 'GrepTool';
export const DESCRIPTION = `
- 适用于任何代码库大小的快速内容搜索工具
- 使用正则表达式搜索文件内容
- 支持完整的正则表达式语法(例如 "log.*Error"、"function\\s+\\w+" 等)
- 使用 include 参数按模式过滤文件(例如 "*.js"、"*.{ts,tsx}")
- 返回按修改时间排序的匹配文件路径
- 当你需要查找包含特定模式的文件时使用此工具
- 当你进行可能需要多轮 glob 和 grep 的开放式搜索时,请改用 Agent 工具
`;
/**
* GrepTool 参数定义
*/
export interface GrepToolArgs {
/** 搜索的正则表达式模式 */
pattern: string;
/** 搜索路径(目录) */
path?: string;
/** 文件模式过滤 */
include?: string;
}
/**
* GrepTool 返回结果
*/
export interface GrepToolResult {
/** 匹配的文件路径 */
matches: string[];
/** 匹配总数 */
count: number;
}
/**
* GrepTool 处理函数
*/
const grepToolHandler = async (
args: GrepToolArgs,
context?: InternalToolContext
): Promise<GrepToolResult> => {
const { pattern, path = context?.cwd || process.cwd(), include } = args;
// 构建 ripgrep 参数
const rgArgs: string[] = [];
// 只返回文件路径
rgArgs.push('-l'); // --files-with-matches
// 文件模式过滤
if (include) {
rgArgs.push('--glob', include);
}
// 搜索模式
rgArgs.push(pattern);
// 执行搜索
const abortSignal = context?.abortSignal || new AbortController().signal;
const results = (await ripGrep(rgArgs, path, abortSignal)) as string[];
return {
matches: results,
count: results.length,
};
};
/**
* GrepTool 工具定义
*/
export const GrepTool: InternalTool<GrepToolArgs, GrepToolResult> = {
name: 'grep_search',
category: 'search',
internal: true,
description: DESCRIPTION,
version: '1.0.0',
parameters: {
type: 'object',
properties: {
pattern: {
type: 'string',
description:
'The regular expression pattern to search for in file contents',
},
path: {
type: 'string',
description:
'The directory to search in. Defaults to the current working directory.',
},
include: {
type: 'string',
description:
'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
},
},
required: ['pattern'],
},
handler: grepToolHandler,
};在实现这个 grepTool 工具函数的时候,使用的是 ripgrep 命令,这个命令执行的速度非常快,比一般的搜索命令要快很多,在构建这个命令执行的函数需要注意两点:
- Mac 系统需要构建执行文件的签名
- 命令执行的时候需要判断,如果本地电脑没有 rg 这个命令,就需要使用 vendor 里面的二进制文件来执行了
使用命令 ripgrep:https://github.com/BurntSushi/ripgrep
import { fileURLToPath } from 'url';
import path from 'path';
import { findActualExecutable } from 'spawn-rx';
import { execFile } from 'child_process';
import { execFileNoThrow } from './execFileNoThrow.js';
import { config } from '../config/env.js';
const __filename = fileURLToPath(import.meta.url);
let __dirname = path.dirname(__filename);
console.log(config.nodeEnv);
if (config.nodeEnv === 'development' || config.nodeEnv === 'test') {
__dirname = path.resolve(__dirname, '..', '..');
}
function ripgrepPath() {
//检查本地是否安装了ripgrep
const { cmd } = findActualExecutable('rg', []);
if (cmd !== 'rg') {
return cmd;
} else {
const rgRoot = path.resolve(__dirname, 'vendor', 'ripgrep');
console.log(rgRoot);
const ret = path.resolve(
rgRoot,
`${process.arch}-${process.platform}`,
'rg'
);
console.log(ret);
return ret;
}
}
export async function ripGrep(
args: string[],
target: string,
abortSignal: AbortSignal
) {
//mac电脑的签名
await macSignature();
const rgPath = ripgrepPath();
return new Promise(resolve => {
execFile(
ripgrepPath(),
[...args, target],
{
maxBuffer: 1_000_000,
signal: abortSignal,
timeout: 10_000,
},
(error, stdout) => {
if (error) {
if (error.code !== 1) {
console.log(error);
}
resolve([]);
} else {
resolve(stdout.split('\n').filter(Boolean));
}
}
);
});
}
//mac电脑的签名
let alreadSign = false;
async function macSignature() {
if (process.platform !== 'darwin') {
return '';
}
alreadSign = true;
console.log('开始验证签名...');
const lines = (
await execFileNoThrow(
'codesign',
['-vv', '-d', ripgrepPath()],
undefined,
undefined,
false
)
).stdout.split('\n');
console.log(lines);
const needsSigned = lines.find(line => line.includes('linker-signed'));
if (!needsSigned) {
console.log('不需要签名');
return;
}
try {
console.log('生成ripgrep签名');
const signResult = await execFileNoThrow('codesign', [
'--sign',
'-',
'--force',
'',
'--preserve-metadata=entitlements,requirements,flags,runtime',
ripgrepPath(),
]);
if (signResult.code !== 0) {
console.log('签名失败', signResult.stderr);
}
console.log('移除应用隔离标记');
const quarantineResult = await execFileNoThrow('xattr', [
'-d',
'com.apple.quarantine',
ripgrepPath(),
]);
if (quarantineResult.code !== 0) {
console.log('移除应用隔离标记失败', quarantineResult.stderr);
}
console.log('签名成功');
} catch (error) {
console.error('签名生成失败', error);
}
}4.1.2、GlobTool 工具实现
实现 GlobTool 工具对象的时候,相应的工具参数的定义:
- pattern:Glob 匹配模式
- path:搜索路径
import { InternalTool, InternalToolContext } from '../types.js';
import { glob } from '../../utils/file.js';
export const TOOL_NAME_FOR_PROMPT = 'GlobTool';
export const DESCRIPTION = `- 适用于任何代码库大小的快速文件模式匹配工具
- 支持像 "**/*.js" 或 "src/**/*.ts" 这样的 glob 模式
- 返回按修改时间排序的匹配文件路径
- 当你需要通过名称模式查找文件时使用此工具
- 当你进行可能需要多轮 glob 和 grep 的开放式搜索时,请改用 Agent 工具
`;
/**
* GlobTool 参数定义
*/
export interface GlobToolArgs {
/** Glob 匹配模式 */
pattern: string;
/** 搜索路径(目录) */
path?: string;
}
/**
* GlobTool 返回结果
*/
export interface GlobToolResult {
/** 匹配的文件路径 */
files: string[];
/** 是否被截断 */
truncated: boolean;
/** 匹配总数 */
count: number;
}
/**
* GlobTool 处理函数
*/
const globToolHandler = async (
args: GlobToolArgs,
context?: InternalToolContext
): Promise<GlobToolResult> => {
const { pattern, path = context?.cwd || process.cwd() } = args;
// 执行 glob 搜索
const abortSignal = context?.abortSignal || new AbortController().signal;
const result = await glob(
pattern,
path,
{ limit: 100, offset: 0 },
abortSignal
);
return {
files: result.files,
truncated: result.truncated,
count: result.files.length,
};
};
/**
* GlobTool 工具定义
*/
export const GlobTool: InternalTool<GlobToolArgs, GlobToolResult> = {
name: 'glob_search',
category: 'search',
internal: true,
description: DESCRIPTION,
version: '1.0.0',
parameters: {
type: 'object',
properties: {
pattern: {
type: 'string',
description:
'The glob pattern to match files (e.g., "**/*.ts", "src/**/*.{js,jsx}")',
},
path: {
type: 'string',
description:
'The directory to search in. Defaults to the current working directory.',
},
offset: {
type: 'number',
description: 'Number of results to skip (for pagination, default: 0)',
},
limit: {
type: 'number',
description: 'Maximum number of results to return (default: 100)',
},
},
required: ['pattern'],
},
handler: globToolHandler,
};这个命令的实现相对来说比较简单,使用 glob 框架就可以搞定
import { glob as globLib } from 'glob';
//匹配搜索文件目录下的文件
export async function glob(
filePattern: string,
cwd: string,
{ limit, offset }: { limit: number; offset: number },
abortSignal: AbortSignal
) {
const paths = await globLib([filePattern], {
cwd,
nocase: true,
nodir: true,
signal: abortSignal,
stat: true,
withFileTypes: true,
});
const sortedPaths = paths.sort((a, b) => (a.mtimeMs ?? 0) - (b.mtimeMs ?? 0));
const truncated = sortedPaths.length > offset + limit;
return {
files: sortedPaths
.slice(offset, offset + limit)
.map(path => path.fullpath()),
truncated,
};
}4.1.3、FileReadTool 工具实现
实现这个文件内容读取的工具对象,增加了一个类似于分页读取内容的功能,避免一次读取太多,相应的工具参数定义:
- file_path:文件路径
- offset:起始行号
- limit:最大读取行数
import { InternalTool, InternalToolContext } from '../types.js';
import { readFileContent } from '../../utils/file.js';
const MAX_LINES_TO_READ = 2000;
const MAX_LINE_LENGTH = 2000;
export const DESCRIPTION = '从本地文件系统读取文件。';
export const PROMPT = `从本地文件系统读取文件。file_path 参数必须是绝对路径,而不是相对路径。默认情况下,它从文件开头读取最多 ${MAX_LINES_TO_READ} 行。你可以选择指定行偏移量和限制(对于长文件特别有用),但建议通过不提供这些参数来读取整个文件。任何超过 ${MAX_LINE_LENGTH} 个字符的行将被截断。对于图像文件,该工具将为你显示图像。`;
/**
* FileReadTool 参数定义
*/
export interface FileReadToolArgs {
/** 文件路径 */
file_path: string;
/** 起始行号(从0开始) */
offset?: number;
/** 最大读取行数 */
limit?: number;
}
/**
* FileReadTool 返回结果
*/
export interface FileReadToolResult {
/** 文件内容 */
content: string;
/** 返回的行数 */
lineCount: number;
/** 文件总行数 */
totalLines: number;
/** 文件路径 */
filePath: string;
}
/**
* FileReadTool 处理函数
*/
const fileReadToolHandler = async (
args: FileReadToolArgs,
context?: InternalToolContext
): Promise<FileReadToolResult> => {
const { file_path, offset = 0, limit } = args;
// 读取文件内容
const result = await readFileContent(file_path, offset, limit);
return {
content: result.content,
lineCount: result.lineCount,
totalLines: result.totalLines,
filePath: file_path,
};
};
/**
* FileReadTool 工具定义
*/
export const FileReadTool: InternalTool<FileReadToolArgs, FileReadToolResult> =
{
name: 'read_file',
category: 'filesystem',
internal: true,
description: DESCRIPTION,
version: '1.0.0',
parameters: {
type: 'object',
properties: {
file_path: {
type: 'string',
description: 'The absolute or relative path to the file to read',
},
offset: {
type: 'number',
description: 'Starting line number (0-indexed, default: 0)',
},
limit: {
type: 'number',
description:
'Maximum number of lines to read (omit to read entire file)',
},
},
required: ['file_path'],
},
handler: fileReadToolHandler,
};这个文件内容读取工具使用的是 Node 的 fs 模块中的文件读取命令,需要重点注意的是文件编码格式要获取,不一定文件编码格式都是 utf-8
//读取文件内容
export async function readFileContent(
filePath: string,
offset: number = 0,
maxLines?: number
): Promise<{ content: string; lineCount: number; totalLines: number }> {
//TODO:获取文件的编码格式 - 默认UTF-8这个原本是需要写一个文件编码获取的函数
const enc = 'utf-8';
//读取文件内容
const content = await readFileSync(filePath, enc);
//按行切割 - 跨平台的兼容要使用"/\r?\n/"
const lines = content.split('\n');
//整理返回结果
const toReturn =
maxLines !== undefined && lines.length - offset > maxLines
? lines.slice(offset, offset + maxLines)
: lines.slice(offset);
return {
content: toReturn.join('\n'),
lineCount: toReturn.length,
totalLines: lines.length,
};
}4.1.4、ListDirectoryTool 工具实现
该工具是文件目录结构查询,工具函数的参数只有一个:
- path:要列出目录的路径
import { InternalTool, InternalToolContext } from '../types.js';
import {
listDirectory,
createFileTree,
printTree,
} from '../../utils/listDirectory.js';
export const DESCRIPTION =
'列出指定路径中的文件和目录。path 参数必须是绝对路径,而不是相对路径。如果你知道要搜索哪些目录,通常应优先使用 Glob 和 Grep 工具。';
/**
* ListDirectoryTool 参数定义
*/
export interface ListDirectoryToolArgs {
/** 要列出的目录路径 */
path?: string;
}
/**
* ListDirectoryTool 返回结果
*/
export interface ListDirectoryToolResult {
/** 文件树格式输出 */
tree: string;
/** 文件路径列表 */
files: string[];
/** 总数 */
count: number;
}
/**
* ListDirectoryTool 处理函数
*/
const listDirectoryToolHandler = async (
args: ListDirectoryToolArgs,
context?: InternalToolContext
): Promise<ListDirectoryToolResult> => {
const { path = context?.cwd || process.cwd() } = args;
// 执行目录列出
const abortSignal = context?.abortSignal || new AbortController().signal;
const files = listDirectory(path, context?.cwd || process.cwd(), abortSignal);
// 创建文件树
const tree = createFileTree(files);
const treeOutput = printTree(tree);
return {
tree: treeOutput,
files,
count: files.length,
};
};
/**
* ListDirectoryTool 工具定义
*/
export const ListDirectoryTool: InternalTool<
ListDirectoryToolArgs,
ListDirectoryToolResult
> = {
name: 'list_directory',
category: 'filesystem',
internal: true,
description: DESCRIPTION,
version: '1.0.0',
parameters: {
type: 'object',
properties: {
path: {
type: 'string',
description:
'The directory path to list. Defaults to the current working directory.',
},
},
required: [],
},
handler: listDirectoryToolHandler,
};在实现该工具函数的中,没有使用递归函数去实现,担心栈溢出,使用的是一个广度搜索,需要注意的是两个格式化的函数
- 将搜索出来的文件数组转换为 Tree 格式的数据结构
- 将 Tree 格式的数据结构转换为空格隔开的目录字符串
import { readdirSync } from 'fs';
import { basename, join, relative, sep } from 'path';
import { getCwd } from './common.js';
export function listDirectory(
initialPath: string,
cwd: string,
abortSignal: AbortSignal
): string[] {
const results: string[] = [];
const queue = [initialPath];
while (queue.length > 0) {
if (results.length > 500) {
return results;
}
if (abortSignal.aborted) {
return results;
}
const path: any = queue.shift();
if (skip(path)) {
continue;
}
if (path !== initialPath) {
results.push(relative(cwd, path) + sep);
}
let children;
try {
children = readdirSync(path, {
withFileTypes: true,
});
} catch (error) {
console.error(error);
continue;
}
for (const child of children) {
if (child.isDirectory()) {
queue.push(join(path, child.name) + sep);
} else {
const fileName = join(path, child.name);
if (skip(fileName)) {
continue;
}
results.push(relative(cwd, fileName));
if (results.length > 500) {
return results;
}
}
}
}
return results;
}
function skip(path: string): boolean {
if (path !== '.' && basename(path).startsWith('.')) {
return true;
}
if (path.includes(`__pycache__${sep}`)) {
return true;
}
return false;
}
/**
* 为了让结果更加可读和节省Token,要创建两个对于结果进行格式化的函数
* - createFileTree
* - printTree
*/
type TreeNode = {
name: string;
path: string;
type: 'file' | 'directory';
children?: TreeNode[];
};
export function createFileTree(sortedPaths) {
const root: TreeNode[] = [];
for (const path of sortedPaths) {
const parts = path.split(sep);
let currentLevel: any = root;
let currentPath = '';
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (!part) {
continue;
}
currentPath = currentPath ? `${currentPath}${sep}${part}` : part;
const isLastPart = i === parts.length - 1;
const existingNode = currentLevel.find(node => node.name === part);
if (existingNode) {
currentLevel = existingNode.children || [];
} else {
const newNode: TreeNode = {
name: part,
path: currentPath,
type: isLastPart ? 'file' : 'directory',
};
if (!isLastPart) {
newNode.children = [];
}
currentLevel.push(newNode);
currentLevel = newNode.children || [];
}
}
}
return root;
}
/**
* eg.
* - src/
* - index.ts
* - utils/
* - file.ts
* @param tree
* @param level
* @param prefix
* @returns
*/
export function printTree(tree: TreeNode[], level = 0, prefix = ''): string {
let result = '';
if (level == 0) {
result += `- ${getCwd()}${sep}\n`;
prefix = ' ';
}
for (const node of tree) {
result += `${prefix}${'-'} ${node.name}${node.type === 'directory' ? sep : ''}\n`;
if (node.children && node.children.length > 0) {
result += printTree(node.children, level + 1, `${prefix} `);
}
}
return result;
}4.2、工具管理
我们定义好这些工具之后,需要将这个工具提供给 LLM,并且 LLM 返回工具执行命令的时候,需要调用相应工具的函数执行,最后将结果返回给 LLM,所以这里需要创建一个工具管理器
我的实现思路:
- 提供一个方法用来注册工具函数:
registerAllTools - 提供一个方法用来获取注册好的工具函数:
getTools - 提供一个工具执行函数:
execute
import {
InternalTool,
InternalToolContext,
FormattedToolDefinition,
} from './types.js';
import { GrepTool } from './GrepTool/GrepTool.js';
import { ListDirectoryTool } from './ListDirectoryTool/ListDirectoryTool.js';
import { GlobTool } from './GolbTool/GlobTool.js';
import { FileReadTool } from './FileReadTool/FileReadTool.js';
const toolsList = [GrepTool, GlobTool, ListDirectoryTool, FileReadTool];
/**
* 工具管理类
* 负责工具的注册、查询和执行
*/
export class ToolManager {
private tools: Map<string, InternalTool> = new Map();
constructor() {
// 在构造函数中自动注册所有工具
this.registerAllTools();
}
/**
* 注册所有工具
* 显式列出所有要注册的工具
*/
private registerAllTools() {
const tools = [...toolsList];
tools.forEach(tool => {
this.tools.set(tool.name, tool);
});
}
/**
* 执行指定工具
* @param name 工具名称
* @param args 工具参数
* @param context 可选的上下文参数(如 abortSignal)
* @returns 工具执行结果
*/
async execute<TArgs = any, TResult = any>(
name: string,
args: TArgs,
context?: InternalToolContext
): Promise<TResult> {
const tool = this.tools.get(name);
if (!tool) {
throw new Error(
`Tool '${name}' not found. Available tools: ${this.getToolNames().join(', ')}`
);
}
try {
const result = await tool.handler(args, context);
return result;
} catch (error) {
console.error(`Tool '${name}' execution failed:`, error);
throw error;
}
}
/**
* 获取所有已注册的工具
* @returns 工具数组
*/
getTools(): InternalTool[] {
return Array.from(this.tools.values());
}
}在使用的时候,因为供应商的不同,对于 api 中的 tool 参数的格式也是不一样的,所以这个需要在定义 LLM 类的时候,自己提供一个工具格式转换方法,例如 Openai 的参数格式是
/**
* Format tools for OpenAI API format
* Converts ToolSet to array of OpenAI tool definitions
*/
private formatToolsForAPI(tools: ToolSet): OpenAITool[] {
return Object.values(tools).map(tool => ({
type: 'function' as const,
function: {
name: tool.name,
description: tool.description || '',
parameters: tool.parameters || {
type: 'object' as const,
properties: {},
},
},
}));
}4.3、LLM 工具调用
Excalidraw 文件:https://gcntfv628ebr.feishu.cn/file/BrjYbAJsPoAwXuxKmkWczabUnOh
.CZ1xcLa4.png)
关于 LLM 的调用中,最核心的思路是:LLM 循环执行并且判断,并且设置最大的循环数,不要陷入死循环
async generate(
userInput: string,
imageData?: any,
stream: boolean = false
): Promise<string> {
try {
// DeepSeek doesn't support images, so we ignore imageData parameter
if (imageData) {
console.warn(
'DeepSeek does not support image inputs. Image data will be ignored.'
);
}
// Build messages array
const messages = this.formatMessages(userInput);
// Get tools if available
const tools = await this.getAllTools();
const hasTools = tools && Object.keys(tools).length > 0;
// Tool calling loop
let iteration = 0;
let finalResponse = '';
while (iteration < this.maxIterations) {
iteration++;
console.log(`\n=== Iteration ${iteration}/${this.maxIterations} ===`);
appendFileSync(
'deepseek.log',
`\n=== Iteration ${iteration}/${this.maxIterations} ===`
);
// Make API call
const response = await this.openai.chat.completions.create({
model: this.model,
messages,
stream,
...(hasTools && { tools: this.formatToolsForAPI(tools) }),
});
if (stream) {
// Handle streaming response
let streamContent = '';
for await (const chunk of response) {
const content = chunk.choices[0]?.delta?.content || '';
streamContent += content;
if (this.eventManager) {
this.eventManager.emit('stream', { content });
}
}
finalResponse = streamContent;
break; // Streaming doesn't support tool calls in chunks
}
const message = response.choices[0]?.message;
const finishReason = response.choices[0]?.finish_reason;
console.log('Finish reason:', finishReason);
if (message?.content) {
console.log('Assistant:', message.content);
appendFileSync('deepseek.log', `\nAssistant:${message.content}`);
}
// Check if model wants to call tools
if (finishReason === 'tool_calls' && message?.tool_calls) {
console.log(`Tool calls detected: ${message.tool_calls.length}`);
appendFileSync(
'deepseek.log',
`\nTool calls detected: ${message.tool_calls.length}`
);
// Add assistant message with tool_calls to context
messages.push({
role: 'assistant',
content: message.content || '',
tool_calls: message.tool_calls,
});
// Execute each tool call
for (const toolCall of message.tool_calls) {
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
console.log(`Executing tool: ${toolName}`, toolArgs);
appendFileSync(
'deepseek.log',
`\nExecuting tool: ${JSON.stringify(toolName)} ${JSON.stringify(toolArgs)}`
);
try {
// Execute tool using tool manager
const toolResult = await this.toolManager?.execute(
toolName,
toolArgs
);
console.log(
`Tool result:${JSON.stringify(toolResult).slice(0, 500)}`
);
appendFileSync(
'deepseek.log',
`\nTool result:${JSON.stringify(toolResult).slice(0, 500)}`
);
// Add tool result to messages
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(toolResult),
});
} catch (error) {
console.error(`Tool execution error:`, error);
appendFileSync('deepseek.log', `\nTool execution error:${error}`);
// Add error message as tool result
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}),
});
}
}
// Continue loop to send tool results back to model
continue;
}
// No more tool calls, return final response
finalResponse = message?.content || '';
break;
}
if (iteration >= this.maxIterations) {
console.warn(`Reached maximum iterations (${this.maxIterations})`);
appendFileSync(
'deepseek.log',
`\nReached maximum iterations (${this.maxIterations})`
);
if (!finalResponse) {
finalResponse =
'I apologize, but I reached the maximum number of iterations while processing your request. Please try breaking down your question into smaller parts.';
}
}
return finalResponse;
} catch (error) {
this.handleError(error, 'DeepseekService.generate');
}
}五、最后
在执行这个搜索代理的时候,我更换了不同的模式进行测试,并且打印出来了搜索策略,我有一些发现可以分享给大家 我简单测试了这几种模型:DeepSeekV3.2、Qwen3、Glm-4.6、Gemini-2.5Pro、Gemini-2.5flash、Claude-sonnet-4.5、GPT-5 我发现表现最好的确实是 Claude 和 GPT,但是里面有一些细微的差别,我的感觉是
- Claude 搜索的策略方向非常正确,而且可以感觉到有步骤的在执行,不会过度的去无脑的搜索,像是一位非常聪明的几何图形问题论证家,解一步,验证一步,在搜索一步,就是感觉优雅
- GPT 的表现也很好,我感觉到它的小心翼翼,是小心翼翼的确定方向,一旦方向确定就会大胆的探索,像是一位稳重成熟的探险家,有谨慎又有强大的执行力