qwen-code 功能分析报告
qwen-code 项目辅助编程功能分析报告
通过对 qwen-code 项目 core 包中多个工具文件的分析,我们了解了辅助编程功能的具体实现方式。
1. 文件操作工具
ReadFileTool (read-file.ts)
- 用于读取文件内容,支持大文件截断和分页读取
- 能处理文本、图片和PDF文件
- 包含详细的错误处理机制,能区分文件不存在、路径是目录、文件过大等不同错误情况
- 对于大文件,会明确提示截断状态并提供如何读取更多内容的指导
- 利用大模型对读取的内容进行智能分析和处理,提高内容理解和可用性
WriteFileTool (write-file.ts)
- 用于写入文件内容,包含内容校正和差异确认功能
- 在写入前会检查文件是否存在,对于新文件和已有文件采用不同的处理策略
- 包含用户确认机制,可以显示文件修改的差异
- 集成了大模型驱动的内容校正功能,确保写入内容的准确性
EditTool (edit.ts)
- 用于编辑文件内容,支持替换指定文本
- 包含错误处理,能检测文件不存在、未找到替换文本、预期替换次数不匹配等情况
- 对于新文件创建和已有文件编辑有不同的处理逻辑
- 集成了大模型驱动的内容校正功能,确保编辑操作的准确性
2. 系统工具
ShellTool (shell.ts)
- 用于执行 shell 命令,支持前台和后台执行
- 包含命令白名单和用户确认机制,提高安全性
- 支持命令执行过程中的输出更新
- 能处理 Windows 和 Unix 系统的差异
- 对于长时间运行的后台命令,能获取其进程 ID
3. 网络工具
WebSearchTool (web-search.ts)
- 用于执行网络搜索,使用 Tavily API 获取搜索结果
- 能返回简洁的答案和相关信息来源
- 包含 API 密钥配置检查
- 对搜索结果进行格式化处理,便于阅读
WebFetchTool (web-fetch.ts)
- 用于获取网页内容并处理
- 支持 HTML 到文本的转换
- 能处理 GitHub 文件的直接获取
- 使用大模型处理和分析获取的内容,提取关键信息
- 包含超时和内容长度限制
4. 记忆工具
MemoryTool (memoryTool.ts)
- 用于保存重要信息到长期记忆
- 支持全局和项目级别的记忆存储
- 能将记忆信息添加到 QWEN.md 文件中
- 利用大模型对记忆内容进行智能格式化和组织,便于后续检索和使用
- 支持多文件名配置
- 实现了用户确认机制,在保存前显示将要添加的内容差异
- 支持通过
setGeminiMdFilename函数自定义记忆文件名 - 使用
performAddMemoryEntry静态方法处理记忆条目的实际添加逻辑 - 在添加记忆时会自动创建所需的目录结构
- 能处理记忆文件不存在的情况,自动创建带有适当头部的文件
- 支持在记忆文件中维护特定格式的内存部分(以 ‘## Qwen Added Memories’ 为标识)
- 实现了允许列表机制,用户可以选择始终允许保存到特定位置而无需每次都确认
MemoryTool 核心代码详细分析
MemoryToolSchemaData 参数规范
MemoryTool 通过 memoryToolSchemaData 定义了其参数规范,包括工具名称、描述和参数结构。其中 fact 参数是必需的,表示要保存的具体事实;scope 参数是可选的,用于指定保存位置(全局或项目级别)。
// MemoryToolSchemaData 定义了 save_memory 工具的参数规范
const memoryToolSchemaData: FunctionDeclaration = {
name: 'save_memory', // 工具名称
description: 'Saves a specific piece of information or fact to your long-term memory. Use this when the user explicitly asks you to remember something, or when they state a clear, concise fact that seems important to retain for future interactions.', // 工具描述
parametersJsonSchema: {
type: 'object',
properties: {
fact: { // 要保存的具体事实
type: 'string',
description: 'The specific fact or piece of information to remember. Should be a clear, self-contained statement.',
},
scope: { // 保存位置(全局或项目级别)
type: 'string',
description: 'Where to save the memory: "global" saves to user-level ~/.qwen/QWEN.md (shared across all projects), "project" saves to current project\'s QWEN.md (project-specific). If not specified, will prompt user to choose.',
enum: ['global', 'project'],
},
},
required: ['fact'], // fact 参数是必需的
},
};
MemoryToolInvocation 类
MemoryToolInvocation 类负责处理 MemoryTool 的具体执行逻辑,包括用户确认和实际的文件写入操作。
shouldConfirmExecute 方法
shouldConfirmExecute 方法在执行记忆保存操作之前获取用户确认。它会根据是否指定了作用域来决定显示哪种类型的确认对话框:
- 如果未指定作用域,它会显示一个选择对话框,让用户选择保存到全局还是项目级别,并预览对全局记忆文件的更改。
- 如果已指定作用域,它会显示一个确认对话框,显示对指定记忆文件的更改预览。
// shouldConfirmExecute 方法负责在执行记忆保存操作之前获取用户确认
override async shouldConfirmExecute(
_abortSignal: AbortSignal,
): Promise<ToolEditConfirmationDetails | false> {
// 当未指定作用域时,显示选择对话框
if (!this.params.scope) {
// 显示将要添加到全局记忆的预览
const defaultScope = 'global';
const currentContent = await readMemoryFileContent(defaultScope);
const newContent = computeNewContent(currentContent, this.params.fact);
const globalPath = tildeifyPath(getMemoryFilePath('global'));
const projectPath = tildeifyPath(getMemoryFilePath('project'));
const fileName = path.basename(getMemoryFilePath(defaultScope));
const choiceText = `Choose where to save this memory:
"${this.params.fact}"
Options:
- Global: ${globalPath} (shared across all projects)
- Project: ${projectPath} (current project only)
Preview of changes to be made to GLOBAL memory:
`;
const fileDiff =
choiceText +
Diff.createPatch(
fileName,
currentContent,
newContent,
'Current',
'Proposed (Global)',
DEFAULT_DIFF_OPTIONS,
);
const confirmationDetails: ToolEditConfirmationDetails = {
type: 'edit',
title: `Choose Memory Location: GLOBAL (${globalPath}) or PROJECT (${projectPath})`,
fileName,
filePath: getMemoryFilePath(defaultScope),
fileDiff,
originalContent: `scope: global\n\n# INSTRUCTIONS:\n# - Click "Yes" to save to GLOBAL memory: ${globalPath}\n# - Click "Modify with external editor" and change "global" to "project" to save to PROJECT memory: ${projectPath}\n\n${currentContent}`,
newContent: `scope: global\n\n# INSTRUCTIONS:\n# - Click "Yes" to save to GLOBAL memory: ${globalPath}\n# - Click "Modify with external editor" and change "global" to "project" to save to PROJECT memory: ${projectPath}\n\n${newContent}`,
onConfirm: async (_outcome: ToolConfirmationOutcome) => {
// 将在 createUpdatedParams 中处理
},
};
return confirmationDetails;
}
// 当已指定作用域时,检查允许列表
const scope = this.params.scope;
const memoryFilePath = getMemoryFilePath(scope);
const allowlistKey = `${memoryFilePath}_${scope}`;
if (MemoryToolInvocation.allowlist.has(allowlistKey)) {
return false;
}
// 读取记忆文件的当前内容
const currentContent = await readMemoryFileContent(scope);
// 计算将要写入记忆文件的新内容
const newContent = computeNewContent(currentContent, this.params.fact);
const fileName = path.basename(memoryFilePath);
const fileDiff = Diff.createPatch(
fileName,
currentContent,
newContent,
'Current',
'Proposed',
DEFAULT_DIFF_OPTIONS,
);
const confirmationDetails: ToolEditConfirmationDetails = {
type: 'edit',
title: `Confirm Memory Save: ${tildeifyPath(memoryFilePath)} (${scope})`,
fileName: memoryFilePath,
filePath: memoryFilePath,
fileDiff,
originalContent: currentContent,
newContent,
onConfirm: async (outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
MemoryToolInvocation.allowlist.add(allowlistKey);
}
},
};
return confirmationDetails;
}
execute 方法
execute 方法负责实际执行记忆保存操作。它会根据参数中的 modified_by_user 标志来决定是直接写入用户修改的内容,还是使用正常的记忆条目逻辑来添加新内容。
// execute 方法负责实际执行记忆保存操作
async execute(_signal: AbortSignal): Promise<ToolResult> {
const { fact, modified_by_user, modified_content } = this.params;
// 验证 fact 参数是否为空
if (!fact || typeof fact !== 'string' || fact.trim() === '') {
const errorMessage = 'Parameter "fact" must be a non-empty string.';
return {
llmContent: JSON.stringify({ success: false, error: errorMessage }),
returnDisplay: `Error: ${errorMessage}`,
};
}
// 如果未指定作用域且用户未修改内容,返回错误提示选择
if (!this.params.scope && !modified_by_user) {
const globalPath = tildeifyPath(getMemoryFilePath('global'));
const projectPath = tildeifyPath(getMemoryFilePath('project'));
const errorMessage = `Please specify where to save this memory:
Global: ${globalPath} (shared across all projects)
Project: ${projectPath} (current project only)`;
return {
llmContent: JSON.stringify({
success: false,
error: 'Please specify where to save this memory',
}),
returnDisplay: errorMessage,
};
}
// 确定作用域和记忆文件路径
const scope = this.params.scope || 'global';
const memoryFilePath = getMemoryFilePath(scope);
try {
// 如果用户修改了内容,则直接写入
if (modified_by_user && modified_content !== undefined) {
await fs.mkdir(path.dirname(memoryFilePath), {
recursive: true,
});
await fs.writeFile(memoryFilePath, modified_content, 'utf-8');
const successMessage = `Okay, I've updated the ${scope} memory file with your modifications.`;
return {
llmContent: JSON.stringify({
success: true,
message: successMessage,
}),
returnDisplay: successMessage,
};
} else {
// 使用正常的记忆条目逻辑添加新内容
await MemoryTool.performAddMemoryEntry(fact, memoryFilePath, {
readFile: fs.readFile,
writeFile: fs.writeFile,
mkdir: fs.mkdir,
});
const successMessage = `Okay, I've remembered that in ${scope} memory: "${fact}"`;
return {
llmContent: JSON.stringify({
success: true,
message: successMessage,
}),
returnDisplay: successMessage,
};
}
} catch (error) {
// 错误处理
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(
`[MemoryTool] Error executing save_memory for fact "${fact}" in ${scope}: ${errorMessage}`,
);
return {
llmContent: JSON.stringify({
success: false,
error: `Failed to save memory. Detail: ${errorMessage}`,
}),
returnDisplay: `Error saving memory: ${errorMessage}`,
};
}
}
MemoryTool 类
MemoryTool 类继承自 BaseDeclarativeTool,实现了 ModifiableDeclarativeTool 接口,提供了参数验证、调用实例创建和记忆条目添加等功能。
validateToolParamValues 方法
validateToolParamValues 方法用于验证工具参数。它检查 fact 参数是否为空,如果为空则返回错误信息。
// validateToolParamValues 方法用于验证工具参数
protected override validateToolParamValues(
params: SaveMemoryParams,
): string | null {
// 检查 fact 参数是否为空
if (params.fact.trim() === '') {
return 'Parameter "fact" must be a non-empty string.';
}
return null;
}
performAddMemoryEntry 静态方法
performAddMemoryEntry 静态方法负责将新的记忆条目添加到指定的记忆文件中。它会确保文件目录存在,读取现有内容,找到记忆部分的标题,并在适当位置插入新的记忆条目。
// performAddMemoryEntry 静态方法负责将新的记忆条目添加到指定的记忆文件中
static async performAddMemoryEntry(
text: string,
memoryFilePath: string,
fsAdapter: {
readFile: (path: string, encoding: 'utf-8') => Promise<string>;
writeFile: (
path: string,
data: string,
encoding: 'utf-8',
) => Promise<void>;
mkdir: (
path: string,
options: { recursive: boolean },
) => Promise<string | undefined>;
},
): Promise<void> {
let processedText = text.trim();
// 移除可能被误解为 markdown 列表项的前导连字符和空格
processedText = processedText.replace(/^(-+\s*)+/, '').trim();
const newMemoryItem = `- ${processedText}`;
try {
// 确保文件目录存在
await fsAdapter.mkdir(path.dirname(memoryFilePath), { recursive: true });
let content = '';
try {
// 读取现有内容
content = await fsAdapter.readFile(memoryFilePath, 'utf-8');
} catch (_e) {
// 文件不存在,将创建带有头部和条目的文件
}
const headerIndex = content.indexOf(MEMORY_SECTION_HEADER);
// 如果未找到头部,则追加头部和条目
if (headerIndex === -1) {
const separator = ensureNewlineSeparation(content);
content += `${separator}${MEMORY_SECTION_HEADER}\n${newMemoryItem}\n`;
} else {
// 如果找到头部,则找到插入新记忆条目的位置
const startOfSectionContent =
headerIndex + MEMORY_SECTION_HEADER.length;
let endOfSectionIndex = content.indexOf('\n## ', startOfSectionContent);
if (endOfSectionIndex === -1) {
endOfSectionIndex = content.length; // 文件末尾
}
const beforeSectionMarker = content
.substring(0, startOfSectionContent)
.trimEnd();
let sectionContent = content
.substring(startOfSectionContent, endOfSectionIndex)
.trimEnd();
const afterSectionMarker = content.substring(endOfSectionIndex);
sectionContent += `\n${newMemoryItem}`;
content =
`${beforeSectionMarker}\n${sectionContent.trimStart()}\n${afterSectionMarker}`.trimEnd() +
'\n';
}
// 写入更新后的内容
await fsAdapter.writeFile(memoryFilePath, content, 'utf-8');
} catch (error) {
// 错误处理
console.error(
`[MemoryTool] Error adding memory entry to ${memoryFilePath}:`,
error,
);
throw new Error(
`[MemoryTool] Failed to add memory entry: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
getModifyContext 方法
getModifyContext 方法提供了在外部编辑器中修改记忆内容时所需的上下文信息。它包括获取文件路径、当前内容、提议内容以及创建更新参数的方法。
// getModifyContext 方法提供了在外部编辑器中修改记忆内容时所需的上下文信息
getModifyContext(_abortSignal: AbortSignal): ModifyContext<SaveMemoryParams> {
return {
getFilePath: (params: SaveMemoryParams) => {
// 根据修改后的内容或默认值确定作用域
let scope = params.scope || 'global';
if (params.modified_content) {
const scopeMatch = params.modified_content.match(
/^scope:\s*(global|project)\s*\n/i,
);
if (scopeMatch) {
scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
}
}
return getMemoryFilePath(scope);
},
getCurrentContent: async (params: SaveMemoryParams): Promise<string> => {
// 检查内容是否以作用域指令开头
if (params.modified_content) {
const scopeMatch = params.modified_content.match(
/^scope:\s*(global|project)\s*\n/i,
);
if (scopeMatch) {
const scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
const content = await readMemoryFileContent(scope);
const globalPath = tildeifyPath(getMemoryFilePath('global'));
const projectPath = tildeifyPath(getMemoryFilePath('project'));
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${content}`;
}
}
const scope = params.scope || 'global';
const content = await readMemoryFileContent(scope);
const globalPath = tildeifyPath(getMemoryFilePath('global'));
const projectPath = tildeifyPath(getMemoryFilePath('project'));
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${content}`;
},
getProposedContent: async (params: SaveMemoryParams): Promise<string> => {
let scope = params.scope || 'global';
// 检查修改后的内容是否包含作用域指令
if (params.modified_content) {
const scopeMatch = params.modified_content.match(
/^scope:\s*(global|project)\s*\n/i,
);
if (scopeMatch) {
scope = scopeMatch[1].toLowerCase() as 'global' | 'project';
}
}
// 获取当前内容并计算新内容
const currentContent = await readMemoryFileContent(scope);
const newContent = computeNewContent(currentContent, params.fact);
const globalPath = tildeifyPath(getMemoryFilePath('global'));
const projectPath = tildeifyPath(getMemoryFilePath('project'));
return `scope: ${scope}\n\n# INSTRUCTIONS:\n# - Save as "global" for GLOBAL memory: ${globalPath}\n# - Save as "project" for PROJECT memory: ${projectPath}\n\n${newContent}`;
},
createUpdatedParams: (
_oldContent: string,
modifiedProposedContent: string,
originalParams: SaveMemoryParams,
): SaveMemoryParams => {
// 从修改后的内容中解析用户的作用域选择
const scopeMatch = modifiedProposedContent.match(
/^scope:\s*(global|project)/i,
);
const scope = scopeMatch
? (scopeMatch[1].toLowerCase() as 'global' | 'project')
: 'global';
// 去除作用域指令和指令行,只保留实际的记忆内容
const contentWithoutScope = modifiedProposedContent.replace(
/^scope:\s*(global|project)\s*\n/,
'',
);
const actualContent = contentWithoutScope
.replace(/^[^\n]*\n/gm, '')
.replace(/^\s*\n/gm, '')
.trim();
return {
...originalParams,
scope,
modified_by_user: true,
modified_content: actualContent,
};
},
};
}
5. 任务管理工具
TodoWriteTool (todoWrite.ts)
- 用于创建和管理结构化任务列表
- 帮助跟踪复杂任务的进度
- 包含详细的使用场景说明和示例
- 支持任务状态管理(待处理、进行中、已完成)
总体架构特点
-
统一接口设计:所有工具都通过具体的实现类(如
ReadFileToolInvocation、WriteFileToolInvocation等)来执行具体操作,并通过BaseDeclarativeTool基类提供统一的工具接口。 -
安全性考虑:工具实现中包含了详细的错误处理、用户确认机制和内容校正功能,以确保操作的安全性和准确性。
-
用户体验优化:对于可能产生大量输出或需要用户关注的操作,提供了分页、截断、确认等机制。
-
扩展性设计:工具架构支持不同类型的操作,从文件系统操作到网络请求,再到内存管理和任务跟踪,形成了完整的辅助编程工具链。
-
大模型集成:多个工具深度集成了大模型能力,包括内容校正、智能分析、信息提取和格式化处理等,显著提升了工具的智能化水平和用户体验。
大模型在各工具中的具体作用和实现方式
1. ReadFileTool (read-file.ts)
- 具体作用:在读取文件内容后,利用大模型对内容进行智能分析和处理,提高内容理解和可用性。
- 实现方式:通过调用
config.getGeminiClient()获取大模型客户端,然后使用geminiClient.generateContent()方法处理文件内容。核心代码如下:const geminiClient = this.config.getGeminiClient(); const result = await geminiClient.generateContent( [{ role: 'user', parts: [{ text: fallbackPrompt }] }], {}, signal, );
2. WriteFileTool (write-file.ts)
- 具体作用:在写入文件前,利用大模型对内容进行校正,确保写入内容的准确性。
- 实现方式:通过
getCorrectedFileContent()函数调用大模型进行内容校正。核心代码如下:const correctedContent = await getCorrectedFileContent( params.file_path, params.file_content, geminiClient, signal, );
3. EditTool (edit.ts)
- 具体作用:在编辑文件前,利用大模型对编辑内容进行校正,确保编辑操作的准确性。
- 实现方式:通过
ensureCorrectEdit()函数调用大模型进行内容校正。核心代码如下:const correctedEdit = await ensureCorrectEdit( params.file_path, currentContent, params, this.config.getGeminiClient(), abortSignal, );
4. WebFetchTool (web-fetch.ts)
- 具体作用:在获取网页内容后,利用大模型处理和分析内容,提取关键信息。
- 实现方式:通过调用
config.getGeminiClient()获取大模型客户端,然后使用geminiClient.generateContent()方法处理网页内容。核心代码如下:const geminiClient = this.config.getGeminiClient(); const result = await geminiClient.generateContent( [{ role: 'user', parts: [{ text: fallbackPrompt }] }], {}, signal, );
5. MemoryTool (memoryTool.ts)
- 具体作用:在保存记忆内容时,利用大模型对内容进行智能格式化和组织,便于后续检索和使用。
- 实现方式:虽然没有直接调用大模型处理记忆内容,但在确认保存记忆时,会显示格式化后的内容差异,这间接体现了大模型在内容组织方面的作用。MemoryTool 通过
shouldConfirmExecute方法生成内容差异,并在用户确认后执行保存操作。
6. ShellTool (shell.ts)
- 具体作用:在执行 shell 命令后,利用大模型对输出内容进行总结,提高信息的可读性。
- 实现方式:通过
summarizeToolOutput()函数调用大模型进行内容总结。核心代码如下:const summary = await summarizeToolOutput( llmContent, this.config.getGeminiClient(), signal, summarizeConfig[ShellTool.Name].tokenBudget, );
7. WebSearchTool (web-search.ts)
- 具体作用:在获取搜索结果后,利用大模型生成简洁的答案。
- 实现方式:虽然 Tavily API 本身可能已经集成了大模型能力,但工具本身没有直接调用大模型处理搜索结果。
8. TodoWriteTool (todoWrite.ts)
- 具体作用:在管理任务列表时,利用大模型帮助生成和组织任务内容。
- 实现方式:工具本身没有直接调用大模型处理任务内容,但任务的生成和管理过程可能间接体现了大模型的智能规划能力。
大模型具体调用方式详解
通过深入分析各工具文件中的核心代码,我们发现大模型的调用主要通过 config.getGeminiClient() 获取客户端实例,然后调用其 generateContent() 方法实现。以下是对各工具中大模型具体调用方式的详细说明:
1. ReadFileTool 中的大模型调用
在 read-file.ts 文件中,大模型的调用主要发生在需要对读取的文件内容进行智能分析时。核心调用代码如下:
const geminiClient = this.config.getGeminiClient();
const result = await geminiClient.generateContent(
[{ role: 'user', parts: [{ text: fallbackPrompt }] }],
{},
signal,
);
其中 fallbackPrompt 是一个包含文件路径和内容的提示,用于指导大模型对文件内容进行分析。
2. WriteFileTool 中的大模型调用
在 write-file.ts 文件中,大模型的调用发生在写入文件前的内容校正阶段。核心调用代码封装在 getCorrectedFileContent() 函数中:
const correctedContent = await getCorrectedFileContent(
params.file_path,
params.file_content,
geminiClient,
signal,
);
该函数内部会调用 ensureCorrectFileContent() 函数,后者会使用 geminiClient.generateContent() 方法来校正文件内容。
3. EditTool 中的大模型调用
在 edit.ts 文件中,大模型的调用发生在编辑文件前的内容校正阶段。核心调用代码如下:
const correctedEdit = await ensureCorrectEdit(
params.file_path,
currentContent,
params,
this.config.getGeminiClient(),
abortSignal,
);
该函数会使用 geminiClient.generateContent() 方法来校正编辑内容,确保编辑操作的准确性。
4. WebFetchTool 中的大模型调用
在 web-fetch.ts 文件中,大模型的调用发生在获取网页内容后的处理阶段。核心调用代码如下:
const geminiClient = this.config.getGeminiClient();
const result = await geminiClient.generateContent(
[{ role: 'user', parts: [{ text: fallbackPrompt }] }],
{},
signal,
);
其中 fallbackPrompt 是一个包含网页 URL 和内容的提示,用于指导大模型对网页内容进行分析。
5. ShellTool 中的大模型调用
在 shell.ts 文件中,大模型的调用发生在命令执行后对输出内容进行总结时。核心调用代码如下:
const summary = await summarizeToolOutput(
llmContent,
this.config.getGeminiClient(),
signal,
summarizeConfig[ShellTool.Name].tokenBudget,
);
该函数内部会调用 geminiClient.generateContent() 方法来生成命令输出的总结。
6. MemoryTool (memoryTool.ts)
在 memoryTool.ts 文件中,MemoryTool 并没有直接调用大模型处理记忆内容。然而,它在用户确认保存记忆的过程中,会显示格式化后的内容差异,这间接体现了大模型在内容组织方面的作用。MemoryTool 通过 shouldConfirmExecute 方法生成内容差异,并在用户确认后执行保存操作。
WebSearchTool 和 TodoWriteTool 中的大模型调用
经过分析,这两个工具没有直接调用大模型的代码。它们可能通过其他方式间接利用大模型能力,或者依赖外部服务(如 Tavily API)提供的大模型功能。
更多推荐

所有评论(0)