高德 AI应用开发 一面
1. 自我介绍
2. 从代码文本到抽象语法树的过程是什么?底层原理是什么?
答案:从代码文本到 AST,本质上是编译前端里的词法分析和语法分析过程。第一步先做词法分析,把原始代码字符串切分成 token,比如关键字、标识符、字符串、操作符、分隔符;第二步做语法分析,根据语言文法把 token 序列规约成语法结构,最终生成 AST。AST 和原始代码最大的区别是它去掉了很多表面细节,比如空格、换行、部分注释,但保留了程序的结构信息,比如函数定义、参数列表、条件分支、调用表达式和作用域关系。
底层原理依赖语言文法,通常是 CFG 或经过工程化改造的语法规则。词法分析一般可用有限状态自动机思想实现,语法分析则常见 LL、LR、LALR、GLR 等方法。做代码 RAG 时,AST 的价值不是拿来替代原始代码,而是把代码从线性文本转成结构化对象,方便做函数级切分、依赖分析、符号提取和跨文件图构建。
import ast
code = """
def add(a, b):
return a + b
"""
tree = ast.parse(code)
print(ast.dump(tree, indent=2))
3. 如果同时存储代码片段和代码注释,RAG 的表结构怎么设计?
答案:如果代码片段和注释同时入库,不能简单当成同一种 chunk 处理,否则注释会污染代码召回,代码也会压制自然语言描述。更合理的做法是把“代码实体”“注释实体”“文档实体”“符号关系”拆开建模。代码实体保存函数、类、方法、文件路径、语言、签名、起止行号、调用关系;注释实体保存 docstring、行内注释、接口说明和自然语言摘要;二者通过 symbol_id 或 chunk_group_id 关联。检索时可以分通道召回,再在融合层做加权。
这种设计的核心是让检索知道“这是代码证据”还是“这是自然语言证据”。用户问“这个函数为什么超时”,可能更需要代码和调用链;用户问“这个接口做什么”,更需要注释和文档。表设计里还要保留 repo、branch、commit、module、language、dependency_scope 等元信息,方便按仓库版本和子模块过滤。
schema = {
"code_chunk": {
"chunk_id": "string",
"symbol_id": "string",
"repo": "string",
"path": "string",
"language": "string",
"content": "text",
"start_line": "int",
"end_line": "int",
"embedding": "vector"
},
"comment_chunk": {
"chunk_id": "string",
"symbol_id": "string",
"path": "string",
"comment_type": "docstring|inline|readme",
"content": "text",
"embedding": "vector"
}
}
4. 说一下 RRF 融合算法,以及它为什么适合混合检索?
答案:RRF 就是 Reciprocal Rank Fusion,核心思想不是直接融合原始分数,而是融合各路召回结果中的排名。公式一般写成:
RRF(d) = Σ 1 / (k + rank_i(d))
其中 rank_i(d) 表示文档 d 在第 i 个召回器里的排名,k 是一个平滑常数。它适合混合检索的原因是不同召回器的打分空间往往不可比,比如 BM25 分数、向量相似度分数、图召回分数尺度完全不同,直接加权容易失真;而排名天然更稳,RRF 可以把多路“各自认为靠前”的候选融合起来。
它尤其适合代码检索这种场景,因为关键词检索能抓住函数名、错误码、接口名,向量检索能抓住语义相近的实现,而图召回能补充调用链相关节点。RRF 的本质是弱化单一路径误差,把多路候选拉到一个统一排序框架里。
def rrf_fuse(rankings, k=60):
score = {}
for rank_list in rankings:
for rank, doc_id in enumerate(rank_list, start=1):
score[doc_id] = score.get(doc_id, 0.0) + 1.0 / (k + rank)
return sorted(score.items(), key=lambda x: x[1], reverse=True)
5. RRF 的权重怎么设计?有没有必要给不同召回器不同权重?
答案:标准 RRF 本身不显式依赖原始分数,但工程上完全可以做 Weighted RRF,也就是给不同召回器分配不同权重。比如关键词检索在报错定位类 query 上通常更强,向量检索在自然语言需求类 query 上更强,图召回在跨文件依赖分析上更强,就可以按 query 类型动态设权。权重不建议固定死,而是根据历史评测集、query 分类结果、仓库类型和问题意图做路由。
如果 query 是“某个错误码在哪处理”,那 lexical 权重应该更高;如果 query 是“这个模块的鉴权逻辑是怎样的”,semantic 和 graph 权重可能更高。真正难点不在公式,而在 query understanding。没有 query 路由能力,RRF 权重再精细也会被错误召回前置污染。
def weighted_rrf(rankings, weights, k=60):
score = {}
for name, rank_list in rankings.items():
w = weights.get(name, 1.0)
for rank, doc_id in enumerate(rank_list, start=1):
score[doc_id] = score.get(doc_id, 0.0) + w / (k + rank)
return sorted(score.items(), key=lambda x: x[1], reverse=True)
6. 重排序阶段的作用是什么?粗排和精排的区别是什么?
答案:召回阶段追求的是别漏掉,重排序阶段追求的是把真正最相关的结果放到前面。代码 RAG 里如果只有召回没有重排,模型经常会拿到一堆“看起来相关但其实不是关键证据”的代码块,比如同名函数、重复封装层、老版本实现或者只出现在注释里的关键词。重排就是在较小候选集上用更贵、更强的模型做相关性判断。
粗排一般轻量、快,目标是把几十万或几千候选压到几百;精排通常用 cross-encoder、多塔增强重排或 LLM-based reranker,把几十上百条候选压到最终 TopK。粗排强调吞吐,精排强调准确性。对代码场景来说,精排最好不仅看 chunk 文本,还要看路径、符号、调用关系和 query 类型。
7. RAG 里单轮推理和多轮推理的区别是什么?代码场景下怎么选?
答案:单轮推理是假设用户问题足够明确,一次检索、一次生成就能完成任务;多轮推理则允许模型在中间做查询改写、补查证据、调用工具、修正假设。代码场景下,两者差异特别明显。比如“某个接口为什么返回 403”,这通常不是一个 chunk 能回答的,可能要查路由、中间件、权限配置、调用链和日志,所以更适合多轮推理;但如果只是“某个函数定义在哪”,单轮就够了。
是否选多轮,取决于问题是否需要跨文件、多跳依赖和动态证据整合。单轮 RAG 更便宜、更稳,但上限低;多轮推理上限高,但更容易引入路径漂移、上下文膨胀和重复调用。真正上线时一般不是二选一,而是先做 query 分类,再路由到单轮或多轮链路。
8. 如果待切分文档是 Markdown 或 PDF,应该怎么做切分?
答案:Markdown 和 PDF 不能用同一套简单定长切分。Markdown 本身有标题层级、代码块、表格、列表、引用块和链接结构,最合理的方式是先解析结构,再按标题树和语义块切分,必要时保留父标题路径。代码块不能被随便截断,否则会破坏语义;表格也不能直接拍平,否则字段关系会丢失。切分后的 chunk 最好带 title_path、block_type、doc_version 等元信息。
PDF 更麻烦,因为它本质上常常只有排版结果,没有语义结构。要先做版面分析、阅读顺序恢复、标题识别、表格提取、页眉页脚去噪,再切分。很多 PDF 直接抽出来是错乱文本,如果不先恢复结构,后面 embedding 和召回都会很差。工程上常用的方案是“布局解析 + 标题树恢复 + 规则切分 + LLM 修复”。
def split_markdown_by_headers(md_text):
chunks = []
current_title = []
current_content = []
for line in md_text.splitlines():
if line.startswith("#"):
if current_content:
chunks.append({
"title_path": current_title[:],
"content": "\n".join(current_content).strip()
})
current_content = []
level = len(line) - len(line.lstrip("#"))
title = line[level:].strip()
current_title = current_title[:level-1] + [title]
else:
current_content.append(line)
if current_content:
chunks.append({
"title_path": current_title[:],
"content": "\n".join(current_content).strip()
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏聚焦 AI-Agent 面试高频考点,内容来自真实面试与项目实践。系统覆盖大模型基础、Prompt工程、RAG、Agent架构、工具调用、多Agent协作、记忆机制、评测、安全与部署优化等核心模块。以“原理+场景+实战”为主线,提供高频题解析、标准答题思路与工程落地方法,帮助你高效查漏补缺.
