-
-
[原创]国际前沿技术 AI+Fuzz 实现细节(CKGFuzzer)
-
发表于: 2025-6-30 15:13 3991
-
本文分析仅代表个人看法,如有错误请指教,好的项目值得深入,如果你也对 LLM + Fuzzer 感兴趣,CKGFuzzer 是个不错的研究项目。
本文 Fuzz 针对库函数,实现细节分析的项目基于 CKGFuzzer ,该项目的论文即将要发表于 2025 年 ICSE 会议的论文 CKGFuzzer,它通过结合代码知识图谱,让大语言模型可以更加高效且准确地生成模糊驱动器。
论文地址:CKGFuzzer: LLM-Based Fuzz Driver Generation Enhanced By Code Knowledge Graph
项目详细注释+Patch版本:Kernel/CKGFuzzer_mowen注释版 at master · mowenroot/Kernel
作者二开版本:mowenroot/AiLibFuzzer: LLM + Fuzzer
CKGFuzzer 是一个将多智能体系统与代码知识图谱相结合的模糊驱动器生成框架,其目标是针对 API 组合生成高效的模糊驱动器,以提升模糊测试的质量和覆盖率。

大致工作流:
1、解析被测目标及其库 API,提取并嵌入代码知识图谱,包括解析抽象语法树,提取数据结构、函数签名、调用关系等关键信息。
2、查询代码知识图谱的 API 组合,关注那些具有调用关系的 API,生成相应的模糊驱动器。
3、编译生成的模糊驱动器,并且通过一个动态更新的库使用情况,修复出现的编译错误。
4、执行编译成功的模糊驱动器,监控库文件的代码覆盖率,对未能覆盖的新路径 API 组合进行变异,迭代该过程并持续进行。
5、使用链式推理分析在模糊测试过程中产生的崩溃,参考包含了真实 CWE 相关的源码示例,来验证这些崩溃的有效性。
CKGFuzzer 好处显而易见,优点也很多,相比于现有的方法(如 PromptFuzz)通过不同的 API 变异组合利用 LLM 生成模糊驱动器,甚至手动编码,CKGFuzzer 能够基于代码知识图的 LLM 驱动的模糊驱动器代理,指导 LLM 为模糊驱动器生成更高质量的 API 组合和基于覆盖引导的变异。
但是详细分析完也是存在一定缺点的:
1、使用的一些库是相对老的、codeql 也是基于老版本,ql 采用老版本编写的。
2、文件目录处理不清晰,代码和项目会混淆在一起,如果你不是很熟悉该项目,可能会被他的文件夹搞混。
3、使用 LLM 时确实采用了记忆的功能,但是没有切片的处理,在实际测试中,利用 LLM 生成 API 描述信息的时候,要发送的信息量太大,会导致tokens 不足。
4、项目存在一定的 BUG,在本文搜索 Patch 可以看到,其实也不能算是 BUG,只是在测试中,环境不一致导致的不兼容。
5、多组 API 时,采用单进程进行 Fuzz,耗时会成倍的增加,这里是可以采用多线程处理的,即使 python 有 Gil 锁。也能大幅度提升效率。
代码执行流程,官方提供了三步骤:
1、repo
从 Target 库中提取信息
2、preproc
构建外部知识库
3、fuzzing
运行模糊测试进程
整体流体大致如下,这里做了简化实际执行没有这么简单,但是总体逻辑大致一样:

分析也采用三步走,下面开始万字的详细分析底层实现,因为 CKGFuzzer 的核心就是 LLM 操作生成高质量的 target_fuzz 和 Fuzz 的覆盖率回调的 API 优化,所有会针对相关操作细致化分析,其他部分会粗略过。
在 10 线程下整个流程大概花了 3小时40分钟

文件位于

添加codeql到环境变量中

这里有个小bug,已经修复
Patch
原程序只有 ../ ,会导致定位不到外层的 docker_shared , 只会在fuzzing_llm_engine

获取传入参数

初始化该 RepositoryAgent 类,完成以下事情.
「1」 维护该类中的字段,
「2」 调用 check_create_folder() 检查输出目录是否存在,如果不存在则新建
「3」 调用 self.init_repo() 使用提供的参数初始化存储库。
「1」 init_repo() 会先检查codeqldb是否存在,以 .successfully_created 文件来判断,如果该文件存在则表示codeqldb存在。如果不存在调用 self._add_local_repo_to_database() 开始创建 database。
「2」 如果当 src_folder 源码目录不存在时,会调用 self.copy_source_code_fromDocker() ,从docker中复制数据出来。
「1」 使用 getpass.getuse() 获取当前用户名,用于后面更改docker 中的文件所有者,因为文件映射中docker权限为root。
「2」 获取项目目录, 目录指向 fuzzing_llm_engine/projects/{project_name},该项目的目录下面包括 build.sh dockerfile project.yaml三个文件,用于构建环境使用。
「3」 维护 image_name 、 build_command变量。 image_name 为 docker的img名称,一般为
"项目name"+"_base_image",build_command 为 构建 docker 的命令。然后执行 build_command 开始构建 docker 镜像
未检查 docker_img 是否存在,这里增加检测
构建可以成功,但是可能会出现两个警告,

警告原因因为,WORKDIR c-ares 未使用绝对目录,建议使用绝对目录。

ENV OLD_LLVMPASS 1 的设置 应该为 ENV OLD_LLVMPASS=1
「4」 在 docker 中 创建 codql 数据库,通过以下命令拉起docker 并构建codeql数据库,这里执行语句带入了我本机的目录,方便理解。并在创建完 codeql database 后,在docker_shared/codeqldb/项目 目录下 创建 .successfully_created 文件来表示数据库创建成功。
可能会因为 /usr/local/bin/compile: line 27: FUZZING_LANGUAGE: unbound variable 报错 ,

需要加上 -e FUZZING_LANGUAGE={args.language}

这里为了方便调试我使用 docker保活,然后进去看docker配置
这个时候可能还有问题,提示没有codeql这个目录,这里我直接用脚本添加,当然也可以手动添加

主要做了,-v 映射文件夹到docker,-e 指定了环境变量, -t 指定启动镜像, -c 执行了codeql 创建数据库语句。
在使用codeql 创建数据库时,编译语句使用 /src/fuzzing_os/wrapper.sh c-ares。
wrapper主要负责 定位到项目源代码处,并对 build.sh 脚本做了检查,然后运行 build.sh 脚本。
就是针对每个项目进行客制化编译了
当 --src_api 存在时,会调用 extract_api_from_head()

「1」 先判断项目源码是否存在,如果不存在需要从docker中复制出来
「2」 调用find_cpp_head_files()遍历并收集项目下面的所有 c/cpp 和 head文件,src_dic 为 项目中的头文件 + c/cpp文件,test_dic 为 项目中的测试文件 。
「3」 调用_extract_API() 分别为两个文件字典提取API信息,最后保存该文件列表{self.output_results_folder}/api/到json中。
「1」 遍历项目中的所有文件,搜集 头文件和 c/cpp 文件,并区分是否包含test,区别是否包含test使用 check_path_test()函数。
「1」 利用上一步 find_cpp_head_files() 收集的文件进行遍历,这里做了一个兼容性读取文件内容。
「2」 利用CppParser.split_code()提取文件中的相应结构,is_return_node: 决定是否返回抽象语法树(AST)节点对象(True 返回 AST 节点,False 返回可序列化信息,如字符串 + 位置)。
「3」将返回的数据保存到同目录的{src}.debug.json文件中。
如果制定了 call_graph 则会调用 extract_src_test_api_call_graph(),你会发现还会调用 extract_api_from_head() ,所以指定了 call_graph 就可以不用使用 --src_api 。

报错在运行 CodeQL 查询时,缺少了一个关键的依赖包:codeql/cpp-all,而且没有对应的 lock 文件来自动安装。

查看了qlack.ymal的格式使用的是libraryPathDependencies,是很老版本使用的,所以这里需要把SDK+引擎都降级为 v2.7.3
引擎地址:Releases · github/codeql-cli-binaries
SDK地址:Release codeql-cli/v2.7.3 · github/codeql
当 SDK+引擎 都降级后才能使用源代码正常执行,否则需要重写 ql 很麻烦。

extract_src_test_api_call_graph
作用:提取源码和测试代码中的 API 调用图(Call Graph)
「1」 根据之前 extract_api_from_head() 提出出来的api文件,把每个文件中的fn_def_list(函数定义列表)提取出来。
「2」 构造一个任务列表 eggs,中包含每个函数所需的信息,[函数名,文件路径,CodeQL 数据库路径,输出结果文件夹路径,LLM 工作目录路径]
「3」 因为整个项目太大了,使用codeql 提取出调用关系又慢,所以必须要使用多线程,使用多线程需要为每个线程拷贝一份数据库,然后并发运行 handle_extract_api_call_graph_multiple_path 函数,执行完后删除拷贝用的临时数据库。
该函数作用主要就是 _extract_call_graph() 的封装
作用:遍历 eggs 并调用 extract_call_graph.sh 从中提取所有函数的调用关系。并将 bqrs 转换为 csv。
做了一些初始化,核心语句就三句
执行这个脚本后就可以使用 codeql 查询所有文件中的所有函数的调用关系以供后面输出fuzz的target文件。
大致会类似执行以下语句
执行完成后 fuzzing_llm_engine/external_database/{项目名称}/call_garph 下会有 每个函数的调用关系的bqrs和csv。

用来构建外部知识库
文件位于 : ./fuzzing_llm_engine/repo/preproc.py
「1」 设置传参格式,project_name(项目文件名)、src_api_file_path(external_database/{项目名}的位置)
「2」 通过用户指定的api_list使用三个函数进行聚合信息, combine_call_graph 聚合 graph 关系, extract_api_from_file 聚合 api 基本信息(函数名、函数所在文件),extract_fn_code 聚合函数代码。
经过聚合后会在 api_combine 中产生三个对应的聚合文件。

就只有两个参数,一个项目名称,还有一个为上一步 repo.py 提取出来的 api 文件所在处()。

就是聚合作用,把用户指定的 api 所对应函数的调用关系全部聚合在一个新的文件中。
「1」 遍历 codebase/call_graph 文件夹中所有以 .csv 结尾的文件,根据用户提供的 api_list.json 来筛选出匹配的 CSV 文件。
「2」遍历匹配的 CSV 文件,通过 pd 库将多个csv 合并为一个并保存到 api_combine/combined_call_graph.csv 。
也是聚合作用,与 combine_call_graph 不同的是,把之前提取的 api 信息进行聚合,聚合两个信息(函数名和函数所在的文件名)
「1」 遍历之前提取的 api 信息,根据用户提供的 api_list.json 来提取出 函数名、函数所在文件。
「2」 聚合 api 信息保存到 api_summary.json并拷贝至 api_combine 下, 现在 api_combine 目录下会有两个文件: graph 聚合、api 聚合。
也是聚合作用,和聚合 api 信息代码几乎相同,聚合了指定 api code
前置

获取当前目录和项目根目录,添加项目根目录到环境变量中

这样可以导入不在当前目录或 site-packages 的模块
执行过程比较长,这里直接拆分,一步一步看。
args_parser() 开始就是传参的设置

以下是参数的大致介绍
参数名称
类型
默认值
描述
--yaml
字符串
""
指定 YAML 配置文件的路径或内容
--gen_driver
布尔类型
True
是否生成模糊测试驱动
--gen_input
布尔类型
True
是否生成输入
--summary_api
布尔类型
True
是否启用 Summary API
--check_compilation
布尔类型
True
是否检查编译
--skip_check_compilation
布尔类型
False
是否跳过编译检查 (会覆盖 --check_compilation)
--skip_gen_driver
布尔类型
False
是否跳过生成模糊测试驱动 (会覆盖 --gen_driver)
--skip_gen_input
布尔类型
False
是否跳过生成输入 (会覆盖 --gen_input)
--skip_summary_api
布尔类型
False
是否跳过 Summary API (会覆盖 --summary_api)
参数处了从用户传入,还有一个从 config.yaml 文件传入。
需要注意的是:该项目是基于 OSS-Fuzz 之后有很多包含 OSS-Fuzz 的基本操作,包括这步的 config.yaml 也是 OSS-Fuzz 的基本配置文件。
便于之后利用这里放上 config.yaml 的具体配置

Agent 分为两大类就是 :生成文本的语言模型、文本向量化模型。
「1」 通过 get_model() 获取 config.yaml 配置中的对应的 文本语言模型,模型有 coder 、 analyzer 两类。
「2」 通过 get_embedding_model() 获取文本嵌入模型,该模型之后用来进行文本向量化。
「3」 设置全局 Settings 的 LLM 设置。Settings 是来自 llama-index里的一个模块,后续的 llm 就会被内部组件自动调用。
因为模型有 coder 、 analyzer 两类,所以在调用时会先行判断需要的版本是否存在,如果不存在则获取另一个版本。
「1」 默认使用 llama3:70b ,可选择的模型有: deepseek、openai、ollama。
「2」 通过 OpenAILike、Ollama 返回 封装了大语言模型服务的客户端类,是 对象实例 ,后续可对该对象直接进行操作。
「1」 默认使用本地部署的 BAAI/bge-small-en-v1.5。
「2」 调用 HuggingFaceEmbedding、OpenAIEmbedding、OllamaEmbedding :返回是 对象实例 用来语义搜索、RAG 等任务
PS :这里的 device 是我修改过的,使用 cpu 进行驱动,原本是使用 cuda:1 。
因为WSL+win10低版本的各种问题,这里我不选择使用 CUDA,需要修改代码使用 CPU 来进行文件向量化。


「1」 test 文件的索引存储 ,以修复生成的模糊驱动程序
「2」 CWE 文件的索引存储

「1」 先设置 Settings 的 llm , 后续会隐式调用 llm
「2」 确定数据和索引目录路径、如果索引已经存在,就加载它。使用 StorageContext.from_defaults() 创建一个加载上下文;
调用 load_index_from_storage() 从本地加载已保存的向量索引;
「3」 否则,构建新的向量索引。使用 SimpleDirectoryReader 加载目录下的所有文件(作为文档对象);使用 SentenceSplitter 把长文档拆成小段(chunk):chunk_size=512:每段最多 512 字符、chunk_overlap=30:相邻段之间有 30 字符重叠。然后调用 VectorStoreIndex.from_documents() 创建索引:会用 Settings.embed_model 来将每段文本转为向量;索引构建完成后,用 .persist() 将构建好的索引持久化到指定目录,方便下次直接加载。
构建好的效果图如下:


这样就在 test_case_index 下就会生成 test 文件 的存储向量索引

和索引存储 test 文件逻辑一致
构建好的效果图如下:

cwe 的对应向量索引

这步可使用 skip_summary_api 跳过。
「1」 获取 API 列表 ,初始化每个 API 的使用次数为 0 。
「2」 创建 FuzzingPlanner 对象 , 这个对象会负责后续 API 相关操作。
「3」 (args.summary_api) 判断是否需要重新生成 API 摘要。
├── 是:调用 LLM 分析代码 ➝ summarize_code()
└── 否:直接使用已有摘要文件
两个分支的区别只有,是否调用 summarize_code()
「4」 将 API 摘要文件复制到 api_combine 目录、通过 extract_api_list 提取函数名列表(确保是带摘要信息的),加载完整的 API 源代码文件和 API 摘要文件
这步做的就是给 API 生成,函数的摘要,对应文件的摘要 。

「1」 载入之前聚合的 api.json code.json
「2」 遍历每个文件和函数,跳过已经生成过摘要的函数
「3」 找出该 API 在 graph 中的所有的调用关系(调用者、或者被调用者) ,把 graph 和 API code 调用 get_code_summary 让LLM 生成函数摘要。
「4」 跳过已经存在的文件摘要,把文件下所有函数的摘要合并,调用 get_file_summary() 让 LLM 生成一个 "文件整体描述"。
「1」 遍历图数据的每一行,把该 API **被调用 **或者 调用 的关系全部提取出来。
「1」 将 api code(api_info) 、api graph(call_graph)、api name(api) 三个信息套在 prompt 中,然后调用 LLM 。
code_summary_prompt 的提示词,主要是说:这是什么函数名称,API 信息,调用图中的字段。
请根据以上信息,为该函数生成一段不超过 60 个词的代码摘要,内容需覆盖以下两个方面:
1、函数的主要功能。2、函数的使用场景
「1」 将 文件名 和 文件下所有函数的摘要合并信息 ( file_info ) 套在 prompt 中,然后调用 LLM 。
file_summary_prompt 提示词
以下是一个 JSON 文件,包含了一个项目文件中的所有 API 信息:
{file}
每个 API 名称后面紧跟着其对应的代码摘要:
{file_info}
请基于每个文件中包含的 API 的代码摘要,为每个文件生成一段不超过 50 个词的文件摘要,内容需涵盖以下两个方面:
请翻译成以下格式:
File Summary: <你的摘要>
需要清楚的一点是 graph 很大,我提供了 4 个 API,提取出来的 graph.csv 有 120MB ,相当大。行数更是恐怖的 28w 多行。

即使只提取相关 API 的 graph 行数也是巨大的,导致发送的 tokens 巨大,在源代码中是没有做切片处理的,但是 deepseek 的 tokens 为 6w 多 ,测试的时候需要发送某个 API 的完整 graph tokens 都达到了 30w 多 ,导致报错。

解决方案:
1、做切片处理
2、对内容进行缩减。
3、更换模型
因为只是 API 的描述信息,所以 graph 少的情况下,影响不算特别大,所以这里使用对 graph 行数进缩减,只发送前 50 行的调用关系。

「1」 调用 build_kg_query() 构建知识图谱,返回值为 4 个语义检索索引,3个图谱索引(这里的file_summary图谱不需要),1 个元数据 code_base。
对象名
描述
pg_all_code_index
所有代码的图谱索引
pg_api_summary_index
API 摘要的图谱索引
pg_api_code_index
代码的图谱索引
pg_file_summary_index
摘要文本的图谱索引
summary_text_vector_index
摘要文本的语义向量索引
all_src_code_vector_index
所有源码的语义向量索引
api_src_vector_index
所有 API 源码的向量索引
code_base
元信息
「2」 将这 4个 图谱索引对象转换为 BaseRetriever 对象,设置 similarity_top_k=3 表示每次最多返回 3 个相似结果,为后续对话、查询、代码摘要生成等提供**“上下文信息检索器”**。
「3」 创建一个 CodeGraphRetriever 支持混合多种信息源检索的对象,汇聚代码、摘要、结构等对象,并将 CodeGraphRetriever 设置到 FuzzingPlanner。
build_kg_query 是 getCodeKG_CodeBase 的封装,这里直接看 getCodeKG_CodeBase()
「1」 提取元数据,类型为 CodeRepository ,通过 get_codebase 获取代码库信息,包含项目的结构、文件路径、函数定义等内容,并可根据 exclude_folder_list 排除部分文件夹
「2」 加载 API 摘要,判断是否初始化图谱。
「2.1」 初始化图谱:
[2.11] 基于graph调用图 CSV 和源码,构建代码调用图,并生成 5 类“文本节点”用于后续向量化:
[2.12] 对以上节点进行语义向量化(构建 Chroma 向量库)
[2.13] 构建 4 个属性图索引(Property Graph Index),每个索引代表一个不同视角的图谱,并结合对应的向量数据库,供后续查询使用。
[2.14] 持久化图谱索引:调用 .persist() 将图谱结构和向量嵌入结果写入磁盘,便于下次直接加载。
「2.2」 加载图谱,不初始化图谱,区别有:是否调用 getCodeCallKGGraph() ,配合已存在的 vector_store 和 property_graph_store 恢复 4 个图谱索引对象。
get_codebase 主要就是 CodeRepository.construct_nodes_fn_doc 的封装。
从 API 数据(源码和头文件)中提取结构化的节点信息,包括文件、函数、结构体、枚举等,为后续构建图谱提供语义实体。
「1」 获取源文件和头文件信息,获取项目路径前缀(这里是我Patch的部分,因为当路径为绝对地址,按照源代码的相对地址就会替换失败,导致后面全部提取失败,提取前缀增加兼容性。后续前缀会被替换为空,然后再将数据集合。)
「2」 遍历所有源文件和头文件,并读取文件内容(读取的时候做了编码兼容),使用 replace() 替换前缀(PS:这点很重要影响后续整个流程),构建文件节点字典 file_id_dict 。
「3」 调用 process_source_files() 提取源代码中的函数定义、全局变量、结构体、枚举节点 (src_fn_def_list_list, src_global_var_node_list, src_struct_node_list, src_enum_node_list)。
「4」 构建 结构体、枚举 定义与别名映射,最终两个字典形如:struct_def: struct 名字 -> struct 定义节点 ,typdef_struct_alias: struct typedef别名 -> 原始 struct 名 。
「5」 构建函数定义映射 fn_dec,每个函数以 “文件路径-函数名” 为键,保存其定义信息。
从预处理后的代码结构信息中提取并标准化出“节点”数据(函数、结构体、枚举、全局变量),为后续图谱建模准备实体数据。
「1」 提取项目路径前缀(这里是我Patch的部分,因为当路径为绝对地址,按照源代码的相对地址就会替换失败,导致后面全部提取失败,提取前缀增加兼容性。后续前缀会被替换为空,然后再将数据集合。)
「2」 遍历所有文件解析每个文件的节点,并跳过指定排除的文件夹
「3」 构建 函数、结构体、枚举、全局变量 4 个节点信息(重点),标准化成 {fid}-{函数名} 的唯一 ID,附带代码和参数信息。
在 construct_nodes_fn_doc 函数中,使用 replace 来替换,这样其实可以抵消项目中有特殊符号导致的问题,但是当使用项目为c-ares 时,我们的路径又是绝对地址这样就会替换失败,导致后面全部提取失败

上面代码在 fuzzing_llm_engine/rag/kg.py 中的 get_codebase() 被调用,原目的是替换路径用来构建源数据为图存储对象

但在后面的 getCodeCallKGGraph() 中调用,使用 "-" 来分割文件名和文件函数,这里就有问题了,当路径为绝对地址不能被replace() 时,就会导致全部提取失败。

解决方案:
1、替换 “-”,用其他更特殊的符号如 "$" 来区别
2、使用相对路径进行 api 的提取
3、修改 replace 函数代码
这里使用第三种方案,添加前缀获取并替换模式
在 construct_nodes_fn_doc() 函数中修改以下代码

在该函数中还会调用 process_source_files() 里面也使用了类似逻辑

修改如下

修改效果,这样修改之后就能规避项目或者路径中自带的 - 干扰。

「1」 创建一个持久化的 Chroma 客户端(连接到磁盘上的数据)
「2」 获取或创建指定名称的 collection(类似于数据库中的表)。
「3」 用 collection 构建 ChromaVectorStore, 封装 collection,变成 llama-index 中支持的向量存储接口。
「4.1」 初始化新的向量索引 :
设置 LLM 和嵌入模型、创建向量存储上下文、构建向量索引并写入 TextNodes。
「4.2」 不初始化新的向量索引,从现有向量数据库加载索引。
根据函数调用关系和代码信息,构建一个用于图查询(Knowledge Graph Query)的知识图谱,其中包括函数、文件、调用关系、函数摘要、源代码等多种信息,最终返回构建好的图谱及其相关节点集合。
「1」 使用 pandas 读取 call_graph_csv 文件、初始化图存储对象。
「2」 初始化实体节点、关系和源代码块的列表,维护 methods_in_codebase(所有项目中定义的方法名) -> 遍历所有函数定义提取函数名。
「4」 逐行构建调用图中的实体节点和边,对每一条调用记录:
「4」 插入节点和边到图谱

「5」构建函数定义和摘要的文本节点 (TextNode),遍历所有函数定义 fn_def :
如果该函数已添加到图谱中,则构建对应的 TextNode(纯文本节点)
因为这块核心区域代码量相对来比较庞大,阅读起来理解起来相对比较复杂,但是他主要就做了如下几件事:
1、主要目的是为了: 获取图谱并持久化图谱索引
2、为了获取图谱就需要获取 **文本节点 **和 关系图,所以要先调用 getCodeCallKGGraph()获取。
3、获取文本节点,就需要代码库信息(项目的结构、文件路径、函数定义等内容),所以要先调用 get_codebase() 获取。
4、文本节点 就是各种字典的集合,关系图 分为 节点、边两个关系。
5、加载 4个 图谱索引,需要 关系图 和 文本节点,所以会在获取 文本节点、关系图 后都存储起来。
在保存之后 kg 目录下会出现 4 个文件夹:

对应四个 Chroma 向量数据库

「1」 gen_agent -> 初始化 FuzzingGenerationAgent,用于生成模糊测试驱动程序
「2」 fix_agent -> 初始化 CompilationFixAgent,用于修复编译错误
「3」 input_agent -> 初始化 InputGenerationAgent,用于生成模糊测试输入
「3」 crash_analyze_agent -> 初始化 CrashAnalyzer,用于分析崩溃信息
这步可使用 skip_gen_driver 跳过。
「1」 尝试获取或生成 API 组合,判断 api 文件是否存在 -> agents_results/api_combine.json,如果存在直接加载 API 组合。不过这个 API 联合体最开始是需要 LLM 来生成的,如果对项目熟悉的话,也是可以自己写 API 组合的。