问题分析: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 的目录生成步骤。

具体流程:

  1. 扫描 repo 里所有 .proto 文件
  2. 用正则解析出 packageservicerpc 定义
  3. 构建固定格式的 WikiStructure JSON
  4. 前端检测到 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 生成方式

前端的核心逻辑是:

  1. 先尝试调用 proto 解析接口获取确定性目录
  2. 如果 proto 接口返回了有效结构,直接使用(跳过 LLM 目录生成)
  3. 并发生成每个页面的具体内容(最多 5 个并行请求)
  4. 如果 proto 接口失败,fallback 到原来的 LLM 生成流程
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐