DeepWiki 优化实战:代码行号与确定性目录生成
问题分析:LLM 为什么算不对行号
在我们的优化版本中,已经使用 tree-sitter 基于 AST 将代码进行拆分存入了本地的向量数据库。
第一版存储的 chunk 格式是这样的:
<file path="src/main/java/com/example/Client.java">
<chunk start_line="503" end_line="581">
package com.example;
import cn.hutool.core.util.ZipUtil;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
// ... 后面的代码
</chunk>
</file>
看起来我们已经告诉 LLM 这段代码从第 503 行开始到第 581 行结束了,但问题在于:chunk 内部的代码是原始文本,没有行号标记。
当 LLM 需要引用某个具体函数的行号时,它必须从 start_line=503 开始,自己数第几行是哪个函数。
这对 LLM 来说太难了——众所周知 LLM 不擅长数学计算,让它去数几十行代码然后算出 503 + 偏移量 = 实际行号,幻觉就不可避免了。
这就好比你让一个文科生做加法题还不让用计算器,虽然能算但准确率堪忧。
优化一:给代码加上行号前缀
既然 LLM 算不准,那最好的办法就是直接把结果给它,让它只需要"读"而不需要"算"。
改动思路
核心改动很简单,在把 chunk 内容发给 LLM 之前,给每一行代码加上实际行号前缀:
def _add_line_numbers(text: str, start_line: int) -> str:
"""给代码文本的每一行添加行号前缀"""
return '\n'.join(
f"{start_line + i}. {line}"
for i, line in enumerate(text.split('\n'))
)
优化前后对比
| 优化前 | 优化后 | |
|---|---|---|
| chunk 内容 | 原始代码文本 | 每行带行号前缀 |
| LLM 行为 | 从 start_line 推算偏移量 | 直接读取行号 |
| 准确率 | 经常偏差 5-20 行 | 基本准确 |
优化前发给 LLM 的数据:
<chunk start_line="503" end_line="581">
package com.example;
import cn.hutool.core.util.ZipUtil;
import com.google.common.collect.Lists;
</chunk>
优化后发给 LLM 的数据:
<chunk start_line="1" end_line="44">
2.
3. import cn.hutool.core.util.ZipUtil;
4. import com.google.common.collect.Lists;
5. import com.google.protobuf.ByteString;
</chunk>
LLM 现在要引用 ZipUtil 的导入行,直接看到前缀 3. 就知道是第 3 行,不需要做任何计算。
具体改动文件
一共改了 4 个文件:
1. api/websocket_wiki.py — 添加 _add_line_numbers() 工具函数,在构建 chunk 内容时加上行号前缀:
if start_line is not None and end_line is not None:
numbered_text = _add_line_numbers(doc.text, start_line)
doc_parts.append(
f'<chunk start_line="{start_line}" end_line="{end_line}">\n{numbered_text}\n</chunk>'
)
2. api/websocket_wiki.py 的 prompt 部分 — 更新 <line_number_rules> 指令,告诉 LLM 直接读取行号前缀而不是自己计算:
"<line_number_rules>"
"Each line in the code context is prefixed with its actual line number (e.g., '100. code here'). "
"When citing source lines, read the line numbers directly from these prefixes. "
"Do not count or calculate line numbers yourself. "
"</line_number_rules>"
Token 成本
每行增加约 4-6 个字符(比如 503.),一个典型的 chunk 30-40 行,大概增加 150 字符。10-20 个 chunks 总共增加约 1500-3000 字符(约 500-1000 tokens),成本基本可以忽略。
优化二:基于 Proto 文件生成确定性目录
第二个问题是 wiki 的目录结构。
DeepWiki 默认的做法是把 repo 的目录树和 README 丢给 LLM,让它自由发挥来生成 wiki 目录(虽然有一些限制提示词,比如输出目录结构的大概要求)。这在通用场景下是合理的,但对我们的内部 Java 项目来说效果不好。
原因很简单:我们所有的业务都是围绕着 gRPC 接口来的,理想的 wiki 目录应该是按 Service 和 RPC 方法来组织的,而不是让 AI 自由发挥出一堆"Architecture Overview"、"Getting Started" 之类的通用章节。
改动思路
写代码读取 repo 里所有的 *.proto 文件,解析出所有的 Service 和 RPC 接口列表,然后直接构建出确定性的目录结构给前端,绕过 LLM 的目录生成步骤。
具体流程:
- 扫描 repo 里所有
.proto文件 - 用正则解析出
package、service、rpc定义 - 构建固定格式的
WikiStructureJSON - 前端检测到 proto 文件存在时,调用这个接口替代 LLM 生成
核心代码:proto_parser.py
新增了一个 api/proto_parser.py 文件,主要做三件事:
扫描 proto 文件:
def find_proto_files(repo_path: str, excluded_dirs=None) -> List[str]:
"""遍历 repo 目录,返回所有 .proto 文件路径"""
skip = set(DEFAULT_EXCLUDED_DIRS) # 排除 vendor、node_modules 等
proto_files = []
for root, dirs, files in os.walk(repo_path):
dirs[:] = [d for d in dirs if d not in skip]
for f in files:
if f.endswith(".proto"):
proto_files.append(os.path.join(root, f))
proto_files.sort()
return proto_files
解析 proto 内容:
_RE_PACKAGE = re.compile(r"package\s+([\w.]+)\s*;")
_RE_RPC = re.compile(
r"rpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s*returns\s*\(\s*(stream\s+)?(\w+)\s*\)",
)
通过平衡大括号匹配来提取 service block,再用正则提取每个 RPC 方法的签名,包括方法名、请求类型、响应类型以及是否是 streaming。
构建 wiki 目录结构:
生成的目录包含 3 个固定章节 + 每个 Service 一个独立章节:
| 章节 | 内容 |
|---|---|
| Overview | 项目总览 |
| System Architecture | 系统架构 |
| Core Features | gRPC 接口汇总 |
| {ServiceName} Service | 每个 RPC 方法一个子页面 |
每个 RPC 方法的页面标题直接用方法签名,比如 GetOrder(GetOrderRequest) returns (GetOrderResponse),非常清晰。
前端改动:page.tsx
在 src/app/[owner]/[repo]/page.tsx 里新增了一个检测逻辑:
// 检测 repo 是否包含 proto 文件
// 如果有,调用 /api/proto/wiki_structure 获取确定性目录
// 如果失败,fallback 到原来的 LLM 生成方式
前端的核心逻辑是:
- 先尝试调用 proto 解析接口获取确定性目录
- 如果 proto 接口返回了有效结构,直接使用(跳过 LLM 目录生成)
- 并发生成每个页面的具体内容(最多 5 个并行请求)
- 如果 proto 接口失败,fallback 到原来的 LLM 生成流程
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)