用 RAG 对比评测 OpenAI o3 与 Claude 3.7 Sonnet:基于 GitHub 代码检索的端到端评测与可观测性实战
导读
本项目以"对 GitHub 开源仓库做 RAG 检索增强问答"为统一基准,让 OpenAI o3-mini 与 Claude 3.7 Sonnet 两款模型回答同一批代码生成类问题,并借助 CometML Opik 构建从链路追踪(Tracing)到自动化评测(Evaluation)的端到端可观测流水线。读完本文,你将掌握:如何用 LlamaIndex 将 GitHub 仓库切分成可检索的代码节点、如何搭建可切换模型的 Streamlit 问答界面,以及如何用 Opik 的 LLM-as-a-Judge 指标对两个模型的生成结果做量化对比。
一、项目定位:为什么要在"代码 RAG"上做模型对比
o3-vs-claude-code 目录围绕一个核心问题展开:针对真实开源仓库的代码问答场景,OpenAI o3-mini 与 Claude 3.7 Sonnet 谁的生成质量更高? 官方 README 给出了明确定位——"Compare Claude 3.7 Sonnet and OpenAI o3 using RAG over code (GitHub)",同时强调该项目"leveraged CometML Opik to build an e2e evaluation and observability pipeline for a RAG application"(借助 Opik 为 RAG 应用构建端到端评测与可观测性流水线)。
换句话说,它把两件事结合在了一起:
- 一个可控的对比实验环境:同一个检索管线、同一份上下文、同一组问题,仅替换底层 LLM,从而把"模型能力差异"从"应用实现差异"中剥离出来;
- 一套可复现的评测机制:用带人工标注参考答案(ground truth)的测试集,配合 LLM 裁判打分,量化比较两个模型的输出质量,而不是靠肉眼观感下结论。
二、整体架构与工作流
从 app.py 与 Opik for LLM evaluation.ipynb 的源码结构看,整个链路可分为四个阶段:
GitHub URL → 克隆仓库 → 按文件类型解析与切分(CodeSplitter / MarkdownNodeParser)
→ 向量化(FastEmbed bge-base-en-v1.5) → 建索引(Qdrant / 内存)
→ 检索(similarity_top_k=4) → 流式问答(o3-mini 或 Claude 3.7 Sonnet)
→ Opik 追踪(LlamaIndexCallbackHandler) → LLM Judge 评测打分
- 交互阶段由 Streamlit 应用承载(
streamlit run app.py); - 评测阶段由 Notebook 承载(
Opik for LLM evaluation.ipynb),评测数据在 data/test.csv 中。
两个阶段复用同一套解析、切分、索引与查询逻辑,保证了"评测的差异只来自模型本身"这一实验前提。
三、环境准备与安装
1. 获取 API Key
按 README 的说明,运行本项目需要三个密钥:
| 密钥 | 用途 |
|---|---|
| Opik API Key | 将追踪与评测结果上报到 Opik 平台(使用云端版时必需) |
| OpenAI API Key | 调用 o3-mini 模型,以及评测阶段作为裁判模型(gpt-4o) |
| Anthropic API Key | 调用 Claude 3.7 Sonnet |
将这些密钥写入项目根目录的 .env 文件。README 中提示参考 .env.example(当前仓库副本中未包含该文件,但结合源码中的 load_dotenv() 调用可推断,典型的键名形如 OPIK_API_KEY、OPENAI_API_KEY、ANTHROPIC_API_KEY,app.py 第 29 行与 Notebook 均通过 python-dotenv 加载环境变量)。
2. 安装依赖
要求 Python 3.11 或更高版本(Notebook 的元数据中也标注了 Python 3.11.11),README 给出的安装命令如下:
pip install opik llama-index llama-index-agent-openai llama-index-llms-openai llama-index-llms-anthropic --upgrade --quiet
除上述包外,从 app.py 的导入语句还可以看到实际运行还依赖以下组件:
streamlit(交互界面)、python-dotenv(环境变量)、nest_asyncio(Notebook 中异步兼容)、qdrant-client与llama-index-vector-stores-qdrant(Qdrant 向量库)、llama-index-embeddings-fastembed(本地嵌入模型)、llama-index-embeddings-huggingface(Notebook 中的备选嵌入方案)。
建议使用虚拟环境安装,避免与系统 Python 环境冲突。
四、运行交互式对比应用
安装完成后,在 o3-vs-claude-code 目录下启动:
streamlit run app.py
启动后在侧边栏可以完成三个关键操作(对应 app.py 第 102-192 行的侧边栏逻辑):
- 选择模型:在
OpenAI o3-mini与Claude 3.7 Sonnet之间切换(第 105-108 行的model_options字典); - 输入 GitHub 仓库 URL:例如
https://github.com/Lightning-AI/LitServe; - 点击 Load:触发克隆、解析、建索引、构建查询引擎的完整流程。
页面顶部标题即为 "Claude 3.7 Sonnet vs OpenAI o3!",并在右侧提供 "Clear ↺" 按钮一键清空会话(对应 reset_chat() 函数)。
五、核心实现剖析:app.py 逐模块解读
1. 模型抽象:一个函数支持双 Provider
@st.cache_resource
def load_llm(model_name, provider="openai"):
if provider == "anthropic":
return Anthropic(model=model_name)
elif provider == "openai":
return OpenAI(model=model_name)
else:
raise ValueError(f"Unsupported provider: {provider}")
(app.py 第 32-39 行)
@st.cache_resource 保证同一模型实例在多次交互中复用,避免重复初始化造成的资源浪费。这是实现"同一个 RAG 管线、仅换模型"的关键抽象点——后续只需在 Settings.llm 上覆盖赋值,即可无缝切换推理后端。
2. GitHub URL 解析与仓库克隆
def parse_github_url(url):
pattern = r"https://github\.com/([^/]+)/([^/]+)"
match = re.match(pattern, url)
return match.groups() if match else (None, None)
def validate_owner_repo(owner, repo):
return bool(owner) and bool(repo)
(app.py 第 43-53 行)
URL 解析通过正则提取 owner 与 repo,随后调用 git clone 将仓库克隆到当前工作目录(第 130-131 行:若本地目录 ./{repo} 不存在则克隆,已存在则直接复用)。克隆失败或 URL 不合法时,界面会分别提示 "Error occurred while cloning the repository, carefully check the url" 与 "Invalid owner or repository"。
3. 按文件类型解析与切分(代码 RAG 的关键)
def parse_docs_by_file_types(ext, language, input_dir_path):
files = glob.glob(f"{input_dir_path}/**/*{ext}", recursive=True)
if len(files) > 0:
loader = SimpleDirectoryReader(
input_dir=input_dir_path, required_exts=[ext], recursive=True
)
docs = loader.load_data()
parser = (
MarkdownNodeParser()
if ext == ".md"
else CodeSplitter.from_defaults(language=language)
)
nodes = parser.get_nodes_from_documents(docs)
return nodes
return []
(app.py 第 55-73 行)
应用默认处理五类文件(第 134-140 行):
| 扩展名 | 解析器 | 语言参数 |
|---|---|---|
.md |
MarkdownNodeParser |
—(按 Markdown 结构切分) |
.py |
CodeSplitter.from_defaults(language="python") |
python |
.ipynb |
CodeSplitter |
python |
.js |
CodeSplitter |
javascript |
.ts |
CodeSplitter |
typescript |
值得说明的是,代码文件与自然语言文档的切分策略完全不同:CodeSplitter 会尽量在类、函数等逻辑边界处切分,保留代码的语义完整性;MarkdownNodeParser 则按标题层级组织节点。这正是"对代码仓库做 RAG"区别于普通文档 RAG 的核心技术点。每个扩展名处理完成后会打印 Found N files ... / Processed N nodes ... 日志,便于排查仓库是否有可检索内容。
4. 向量化与索引构建
Settings.embed_model = FastEmbedEmbedding(model_name="BAAI/bge-base-en-v1.5")
try:
index = create_index(nodes) # Qdrant 向量库路径
except:
index = VectorStoreIndex(nodes=nodes) # 内存索引兜底
(app.py 第 150-154 行)
嵌入模型采用 BAAI/bge-base-en-v1.5(通过 FastEmbedEmbedding 本地运行,无需单独部署 Embedding 服务)。索引构建有两条路径:
- 首选 Qdrant:
create_index()(第 77-86 行)会为每个会话生成一个uuid4()唯一集合名(chat_with_docs_{uuid}),通过QdrantVectorStore与StorageContext写入向量库,天然支持会话隔离与持久化; - 兜底内存索引:从源码结构看,app.py 第 152 行调用
create_index(nodes)时未传入client参数(而函数签名要求该参数),因此该调用会抛出TypeError并被except捕获,实际大多走VectorStoreIndex(nodes=nodes)的内存索引路径;Notebook 中的评测流程则直接使用内存索引。这一点体现了"可插拔存储"的设计:向量库不可用时应用仍可运行。
5. 检索与生成参数
query_engine = index.as_query_engine(streaming=True, similarity_top_k=4)
(app.py 第 159 行)
streaming=True:开启流式输出,前端逐 token 渲染;similarity_top_k=4:检索阶段取与问题最相似的 4 个节点作为上下文注入 Prompt。
6. 自定义 Prompt 模板
qa_prompt_tmpl_str = (
"Context information is below.\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"Given the context information and your knowledge, I want you to think step by step "
"to answer the query in a crisp manner, incase case you don't know the answer say 'I don't know!'.\n"
"Query: {query_str}\n"
"Answer: "
)
qa_prompt_tmpl = PromptTemplate(qa_prompt_tmpl_str)
query_engine.update_prompts(
{"response_synthesizer:text_qa_template": qa_prompt_tmpl}
)
(app.py 第 162-175 行)
该模板要求模型基于上下文"step by step"思考并简洁作答,无法回答时明确输出 I don't know!——这一约束对评测尤其重要,可以抑制模型的幻觉式作答。而 Notebook 中的评测版模板更进一步(见下文第八节),强制要求回答必须包含代码片段,与"代码生成评测"的定位完全对齐。
7. 流式聊天界面
streaming_response = query_engine.query(prompt)
for chunk in streaming_response.response_gen:
full_response += chunk
message_placeholder.markdown(full_response + "▌")
message_placeholder.markdown(full_response)
(app.py 第 231-239 行)
用户输入通过 st.chat_input 捕获,追加到 st.session_state.messages 维护会话历史;回答期间用 ▌ 光标模拟打字效果,完成后将完整回答写回历史记录。reset_chat() 则负责清空 messages 与 context 并触发 gc.collect() 释放内存。
六、评测数据集:data/test.csv
评测样本存放在 data/test.csv,采用 question,answer 两列结构:question 是待评测问题,answer 是人工编写/认可的参考答案(ground truth)。从数据内容看,测试集围绕 LitServe 框架(Lightning AI 的推理服务库)设计,覆盖了五种典型场景:
- SimpleLitAPI:输入一个数,返回平方与立方的和(
server.py完整示例); - 文本嵌入 API:基于 SentenceTransformer(BAAI/bge-large-en-v1.5)+ LitServe 构建;
- RAG API:LlamaIndex + Qdrant 向量库 + Ollama 本地 llama3.2 的组合;
- Whisper 私有 API:将 OpenAI Whisper(
large模型、GPU 加速)封装为推理服务; - 随机森林部署:加载
model.pkl并封装为分类 API。
设计精巧之处在于:所有问题都是"描述性需求 + 具体约束"(如指定模型、端口、批大小、加速器等),答案要求产出完整可运行的代码。这种设定对模型的能力区分度很高——不仅能检验"是否理解需求",还能检验"是否产出符合约束的语法正确代码",天然适配 LLM-as-a-Judge 的自动打分。
七、用 Opik 构建端到端可观测性
1. 初始化与配置
import opik
opik.configure(use_local=False)
(Notebook 第一个代码单元)
use_local=False 表示使用 Opik 云端(需在 .env 中配置 Opik API Key);若要本地部署 Opik 服务,可改为 use_local=True 并指向本地地址。
2. 追踪 RAG 全链路调用
from llama_index.core.callbacks import CallbackManager
from opik.integrations.llama_index import LlamaIndexCallbackHandler
opik_callback_handler = LlamaIndexCallbackHandler()
Settings.callback_manager = CallbackManager([opik_callback_handler])
这是整个可观测性体系的基石:LlamaIndexCallbackHandler 会自动把 LlamaIndex 内部所有操作(文档加载、节点切分、embedding、检索、LLM 调用、响应合成)以 Trace/Span 的形式上报到 Opik。开发者无需在业务代码中埋点,就能在 Opik 平台看到一次问答从"检索到哪 4 个节点"到"模型生成了什么"的完整调用链,极大降低了 RAG 应用的调试成本。
3. 在评测中复用同一套 RAG 管线
Notebook 中的 setup_chat_engine(github_url, model_provider=...) 与 app.py 高度同构:解析 URL → 克隆 → 按文件类型切分 → 建索引 → 配置 LLM → 构建 streaming=True, similarity_top_k=4 的查询引擎。值得注意的两点差异:
- 支持
Claude 3.5 Sonnet(claude-3-5-sonnet-20240620)作为第三个可选项; - 评测版 Prompt 强制要求代码输出:
Given the context information above, you must always include a code snippet in your response.
Think step by step to answer the query, and then provide a relevant code example that demonstrates the concept.
...
If you don't know the answer, say 'I don't know!' but still provide a minimal code example of what you think might work.
Notebook 示例中以 https://github.com/Lightning-AI/LitServe 作为评测仓库(与 test.csv 的题目主题一致),调用方式为:
model_name = 'Claude 3.7 Sonnet'
query_engine = setup_chat_engine(github_url, model_provider=model_name)
response = query_engine.query("What is this repo about?")
八、LLM-as-a-Judge:自动化评测实现
1. 创建评测数据集
from opik import Opik
client = Opik()
dataset = client.get_or_create_dataset(name="Eval Code Generation")
评测前需要把 test.csv 中的问答对导入该数据集(Notebook 已创建名为 Eval Code Generation 的数据集)。
2. 包装评测任务
from opik import track
@track
def my_llm_application(input: str) -> str:
response = query_engine.query(input)
return str(response)
def evaluation_task(x):
return {"output": my_llm_application(x['input'])}
@track 装饰器让每次评测调用也被记录到 Opik,实现"评测本身可观测"。
3. 自定义 LLM Judge 指标
Notebook 定义了一个完整的 LLMJudgeMetric 类(继承 opik.evaluation.metrics.base_metric.BaseMetric),核心逻辑如下:
- 裁判模型:默认
gpt-4o(通过openai客户端调用); - 打分维度:正确性(是否实现相同功能)、完整性(是否包含必要组件)、效率(实现方式是否同样高效);并明确"只要功能等价就给高分"、"只关注代码与功能,忽略文本";
- 输出格式:强制要求裁判返回严格的 JSON:
{"score": <0~1之间的数>},其中 0 表示完全不相关/错误,1 表示与参考答案功能等价; - 落库:通过
score_result.ScoreResult(name=..., value=...)将分数封装为 Opik 指标。
code_quality_metric = LLMJudgeMetric()
4. 运行评测实验
from opik.evaluation import evaluate
evaluation = evaluate(
dataset=dataset,
task=evaluation_task,
experiment_name=model_name, # 用模型名区分实验
scoring_metrics=[code_quality_metric],
experiment_config={"model": "gpt-3.5-turbo"}
)
这就是整个对比实验的"收敛点":为每个模型各跑一次 evaluate,experiment_name 分别设为 "OpenAI o3-mini" 与 "Claude 3.7 Sonnet",即可在 Opik 平台同一数据集上并排对比两个模型的平均分、逐样本得分分布与失败案例。experiment_config 记录实验元信息(如推理模型),保证实验可复现、可追溯。
九、对比实验的设计要点与落地建议
综合整个项目,这套"代码 RAG + 双模型对比 + Opik 评测"的方案在工程上有几个值得借鉴的设计决策:
- 控制变量:解析、切分、嵌入、检索、Prompt 全部固定,唯一变量是底层 LLM,使得分差异能归因到模型本身;
- 面向代码的评测口径:测试题要求产出完整可运行代码,裁判按"功能等价性"而非"文本相似度"打分,避免传统 ROUGE/BLEU 在代码场景的失效问题;
- 评测与交互解耦:交互看效果用 Streamlit(app.py),批量量化用 Notebook(
Opik for LLM evaluation.ipynb),两者共享核心管线代码,可独立演进; - 可观测性贯穿始终:追踪(Callback Handler)与评测(Judge 指标、实验管理)都收敛到 Opik 单一平台,链路视图 + 得分视图 + 数据集管理一站式完成。
注意事项与前提:本方案依赖 OpenAI、Anthropic 与 Opik 三方服务的 API Key,且需要能访问外网以克隆仓库与下载模型/依赖;嵌入模型与切分参数(similarity_top_k=4)对结果有影响,若要横向对比其他评测集,建议保持这些参数一致并记录在 experiment_config 中。
十、在仓库中继续深入
如果你想进一步研究或复用本方案,可在仓库内继续查看以下文件:
- o3-vs-claude-code/app.py:Streamlit 交互应用完整实现(模型抽象、仓库解析、代码切分、向量检索、流式问答);
- o3-vs-claude-code/Opik for LLM evaluation.ipynb:Opik 追踪与 LLM Judge 评测的完整 Notebook(含自定义指标类与
evaluate调用); - o3-vs-claude-code/data/test.csv:评测数据集(问题 + 参考答案),可自行扩充更多代码生成场景;
- o3-vs-claude-code/data_prep.ipynb:数据准备 Notebook(当前为空,可结合 data/test.csv 自行补充生成逻辑);
- 仓库中还有多个可交叉参考的项目:deploy-agentic-rag(LitServe 部署 RAG API)、github-rag(本地代码仓库问答)、eval-and-observability(Opik 评测与可观测性专项)等,可与本项目的评测管线相互印证。
通过本项目的实践,你将同时掌握两条硬技能:基于 LlamaIndex 对代码仓库构建 RAG,以及用 Opik 搭建"追踪 + 评测 + 实验对比"的端到端可观测体系——这两者正是当前 RAG 应用从原型走向生产的必备能力。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00