索引过程始于原始数据,主要包括两种类型的内容:一种是C#源代码文件,另一种是XML定义文件(Defs)。索引的目标不是完整地存储整个仓库,而是提取可检索且易于理解的最小单元。这些单元应保留足够的上下文以便阅读,并足够小以进行精确匹配和生成语义向量。因此,第一步是扫描文件系统,逐个读取和分类文件,并记录基本元数据,如文件路径、编码、时间戳等作为索引项的输入标识符。元素包括类、函数、变量定义以及XMLDEFs。
读取文件后,需要将长文本分割成多个块。对于C#代码,这通常通过语法标记完成:分片基于类、方法、属性或注释块,并记录每个片段在源文件中的起始和结束偏移量,以便未来精确回溯。对于XML Def,通常保持定义节点的整个字符串表示为一个块,因为Def本身在语义上相对独立且自包含。在分割时,将为每个块生成标题(如符号名称或Def名称)、摘要片段(用于快速预览)以及用于检索的正文文本。分割策略会影响召回质量:过大的块可能导致上下文破坏,而过小的块可能会丢失相关信息。
文本块生成后,索引过程将并行地将每个块发送到两个不同的存储层:用于传统词法检索的倒排索引(此处使用Lucene实现)以及用于语义检索的向量索引。写入倒排索引时,将一起存储结构化字段,如块标题、类型(C#或XML)、路径、源偏移量和标签。这些字段支持过滤和将结果定位回源文件。向量部分首先需要将文本块转换为向量表示,这通常由外部嵌入服务完成。项目支持批量参数配置以生成嵌入,以便根据不同的显存或CPU条件调整批处理大小。生成向量后,这些向量将写入向量索引并映射到相应的块ID。由于C#对huggingface的支持类似于吃狗屎,我专门设置了一个Python服务器在后台嵌入服务,地址为127.0.0.1:5000,这样可以在每次查询时节省冷启动时间。说到向量操作,项目利用了.NET的SIMD加速特性,通过System.Numerics.Tensors库实现了快速的向量点积运算,释放了现代CPU硬件并行计算的全部潜力。
索引的另一关键部分是图关系构建。静态分析器在解析时会搜索源代码和XML之间的有意义的引用关系,例如继承、方法调用、字段引用、XML到C#绑定(其中Def指定了组件或类的使用)等。每当发现一个关系时,都会将其写入图存储作为图的有向边。图的节点和边属性存储在tsv中,并使用两种额外的(行和列)压缩稀疏矩阵表示形式进行存储,以便它们可以在O(1)时间内写入,向下读取关系,向上读取关系。同时,由于使用二进制位存储而不是完整的边信息文本,我将原始B树数据库的6.2GB大小压缩到了400MB。这些边补充了文本相似性的不足,使得跨层查询(如“哪些Def使用了这个C#组件”)能够高效回答。
检索和召回有四个工具:粗略搜索、双向依赖树搜索(uses和used_by)以及全代码召回(get_item)。我的核心思想是尽量减少直接返回无用源代码块,以减少大型模型上下文中不必要的噪声。大型模型代理的典型搜索路径是先对问题进行粗略搜索,获得候选元素列表,然后大型模型选择最有趣的元素名称并召回整个代码;或者,通过使用get_uses和get_used_by找到依赖关系,获得元素列表,选择最有趣的元素名称并召回整个代码。如果在粗略搜索的五个结果中直接实现全代码召回,只有一段有用的代码可用,那么用无用信息填充大型模型上下文会导致性能迅速下降。然而,如果只返回一个结果,粗略搜索无法找到正确答案的概率较大(很可能正确的答案排在第二位!),而且除了第二次粗略搜索外,大型模型无法通过其他更模糊的搜索条件获取候选搜索结果列。在这种情况下,大型模型只能不断尝试不同的搜索,或者简单地采用错误的信息,导致用户血压升高。相反,最好将搜索分成两步,充分利用大型模型的判断能力,减少不必要的信息噪声。
粗略搜索分为两步:首先,使用Lucene模糊搜索和BM25字面相似性排序快速索引大约80000个元素,选出1000个最相似的。然后,基于向量相似性对这1000个项目进行语义相似性评分,选出五个候选内容。这确保了速度并且不容易错过正确答案。最初,我计划将字面相似性得分和向量语义相似性得分相加,但未能充分发挥各自的优势(快速稳定的字面翻译能力和强大的向量理解能力),反而使噪音淹没了信息;例如,当搜索“爪猎人蜱虫”时,如果仅在最后一步使用向量排序,“总营养消耗每天”可以排名第三。然而,如果给一些字面匹配度加权,一堆xxxxx.Tick()会堆积在前面,导致找不到任何东西。
图检索没有特别设计,毕竟我的三个图数据库组合已经足够快,每条边属性的注册也有利于根据结果类型进行预筛选,减少噪声。顺便说一下,在测试过程中,大型模型突然想到要搜索Verse.Thing的引用关系,MCP直接返回了26000条数据,导致上下文溢出。后来,我重写了返回内容的排序,确保排序稳定性,并在此基础上开发了一个简单的分页机制,以实际请求次数和令牌使用量为代价确保了一些安全性。
在部署和性能方面,项目提供了嵌入生成和向量索引的批处理大小调整参数,以平衡不同显存和硬件配置下的速度和资源利用率。实际操作中,Lucene的检索延迟通常非常小,而语义重新排序和向量搜索则随着候选集和向量模型的大小而变化。因此,在资源受限环境中,可以选择关闭嵌入或减少批次以确保稳定运行。
最直接启动该系统进行交互或集成的方式是运行MCP服务器组件,该服务进程使用标准输入输出或JSON-RPC协议暴露其接口。外部AI助手可以向它发送检索和导航请求并接收结构化响应。您还可以在命令行模式下运行各种工具命令,用于索引构建、混合检索或图查询,方便离线测试和脚本编写。
尽管Lucene->嵌入的粗略搜索阶段听起来很好,结合了两者的优势,但在实际测试和实验中发现,Lucene过滤掉的正确选项比预期的更多。因此,这种过程中牺牲的准确性并不值得我们在这几秒钟内获得的额外检索速度。相反,由于LLM无法获得所需信息,可能会继续进行搜索,导致更大的开销。在这种情况下,我决定放弃混合两阶段检索架构,直接采用全向量检索。至于召回速度,我相信Faiss可以提供足够的算法加速。当然,我们的Lucene和BM25的重新排列并非完全无用:在剩下的三个工具中,由于输入需要准确的元素名称,经常导致召回失败。我们可以使用这个高效快速的检索和字面相似性评分系统代替原来的直接匹配,提供更强健性和提高代理工作流程的平滑度。这一经验也是一个教训:面对模型的强大理解能力,任何人工工程方法都可能成为约束,不仅无法整合相应优势,还可能因其固有的缺点极大地限制模型的性能。在工程设计中,传统的工程方法不能将模型视为填补某一缺陷的桥梁,将模型插入原有的架构执行简单且可预测的任务,限制模型的信息输入和操作权限。相反,我们应该退一步,提供辅助信息,充分利用模型的能力。
在一些其他实验中,我对get_uses和get_used_by工具进行了高强度测试,发现尽管这两个工具具有稳定且完整的召回,但对于许多类来说,由于我们详细的关联提取和Rimworld源代码的复杂性,提取上下游关系可能会返回大量数据。这种情况不仅限于一两个核心定义类,如ThingComp和Pawn,也发生在许多实现特定功能的代码元素中。这给大型模型的判断带来了大量噪声。因此,我计划设计一个权重算法,为图的边分配优先权重,并按逆序对查询依赖关系的结果进行排序。这将增加重要链接位于顶部的概率,并间接减少大型模型获取信息的噪声。边的权重可能因边的类型、节点的重要性指数(我计划使用经典的Google PageRank算法计算节点的重要性指数,这不应产生巨大的计算成本)甚至基于两个名称之间的字面相似性计算权重因子而有所不同。然而,我对这个权重算法有一些担忧,因为它是由人工直观设计的,可能无法反映其理论重要性,甚至可能对检索效率产生负面优化效果。然而,如果寻求非人工设计的最佳解决方案权重方法,甚至无法定义问题,从算法角度来看,无从下手。我不想仓促决定权重,所以看来未来大量的实验是不可避免的。😩🤌 哇哦。
还有一些小代理,比如一些非常热心的朋友希望调用远程嵌入模型API服务,而不局限于本地模型。建议在初始化时一键启动5000端口嵌入服务器。还有一些朋友希望使用SSE传输而不是Stdio,以避免Stdio方法的一些缺点并便于局域网传输。这些都是非常好的建议,相关的更改已被列入议程。请耐心等待(正在进行中,正在进行中.jpg)。
我真诚感谢Edge World模组社区的每一位热情支持者。我爱你们所有人。该项目遵循MIT协议,这意味着我希望我的代码可以自由地被太阳系内的所有生物使用。在交流和讨论的过程中,我也收获颇丰。因此,感谢你们所有的支持。
构建或更新索引,生成倒排索引、向量嵌入和图关系数据。
cd src\RimWorldCodeRag
dotnet run -- index --root "..\..\RimWorldData"
cd src\RimWorldCodeRag
dotnet run -- index --root "..\..\RimWorldData" --force
cd src\RimWorldCodeRag
dotnet run -- index --root "..\..\RimWorldData" --python-batch 1 28
cd src\RimWorldCodeRag
dotnet run -- index --root "..\..\RimWorldData" --embedding-server "http://127.0.0.1:5000"
注意: --force将强制清除/刷新现有索引并从头开始重建,适用于修复完整重建后的字段存储或拆分规则更改。常规更新可以去掉--force以启动增量构建,这更快并保留未更改的数据。
提示: 最优批处理大小与VRAM有关。在我的Geforce RTx4060笔记本电脑上,VRAM为16GB,批处理大小为256-512较为合适。超过这个值会导致一些数据动态迁移到CPU,大大降低嵌入效率。您可以多次尝试以找到最优批处理大小。
提示: 使用--embedding-server可以连接到正在运行的嵌入服务器,避免每次批处理加载模型的开销。需要先运行嵌入服务器:.\scripts\start-embedding-server.ps1
cd src\RimWorldCodeRag
dotnet run -- rough-search --query "weapon gun" --kind def --max-results 10
dotnet run -- get-uses --symbol "xml:Gun_Revolver" --kind csharp
dotnet run -- get-used-by --symbol "RimWorld.CompProperties_Power" --kind xml
dotnet run -- get-item --symbol "RimWorld.Building_Door" --max-lines 200
dotnet run -- get-item --symbol "xml:Door"
--kind 支持 csharp/cs 或 xml/def,用于仅在某个级别(C#或XML)查询--max-results 控制候选数量的返回,--max-lines 控制返回的源代码最大行数# 克隆或下载项目
cd RiMCP_hybrid/
# 放置 RimWorld 数据(必需)
# 从您的 RimWorld 安装目录复制Def数据:C:\Program Files (x86)\Steam\steamapps\common\RimWorld\Data
# C#源码:通过ILSpy或者dnspy导出
# 放置到:RimWorldData/(与此 README 同级)
# 如果我在仓库里直接上传边缘世界源码,泰南会告死我,懂吗
# 放置嵌入模型
# mkdir -p src/RimWorldCodeRag/models/
# 下载模型如 e5-base-v2 到:src/RimWorldCodeRag/models/e5-base-v2/
# 注意:此项目对 e5-base-v2 有特殊优化(添加 "query: " 和 "passage: " 前缀)
# 其他模型也可工作但可能性能略有下降,其实我不清楚,就一些其他modder的使用体验来看,并没有多大影响
# 设置 Python 虚拟环境并下载模型(必需,在构建前运行)
# Windows PowerShell:
.\scripts\setup-embedding-env.ps1
# Linux/macOS:
./scripts/setup-embedding-env.sh
# 构建所有组件(因为 .gitignore 排除了构建输出)
dotnet build
# 从 RimWorld 数据创建搜索索引
cd src/RimWorldCodeRag
dotnet run -- index --root "..\..\RimWorldData"
# 终端 1:启动嵌入服务器(保持运行)
# Windows PowerShell:
.\scripts\start-embedding-server.ps1
# Linux/macOS:
./scripts/start-嵌入服务器.sh
# 终端 2:启动 MCP 服务器
cd src\RimWorldCodeRag.McpServer
dotnet run
在运行MCP服务器之前设置这些变量:
# 必需:指向第 3 步创建的索引文件夹路径
$env:RIMWORLD_INDEX_ROOT = "c:\path\to\RiMCP_hybrid\index"
# 可选:嵌入服务器 URL(默认:http://127.0.0.1:5000)
$env:EMBEDDING_SERVER_URL = "http://127.0.0.1:5000"
编辑 src/RimWorldCodeRag.McpServer/appsettings.json:
{
"McpServer": {
"IndexRoot": "c:/path/to/RiMCP_hybrid/index",
"EmbeddingServerUrl": "http://127.0.0.1:5000"
}
}
cd src\RimWorldCodeRag.McpServer
# Windows PowerShell:
.\test-mcp.ps1
# Linux/macOS(如果可用):
./test-mcp.sh
# 在一个终端启动服务器
dotnet run
# 在另一个终端测试
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}' | dotnet run
创建或编辑 %APPDATA%\Code\User\globalStorage\mcp-servers.json:
{
"mcpServers": {
"rimworld-code-rag": {
"