Llama 4 与 DeepSeek-R1 的 RAG 对决:基于 LlamaIndex + Opik 的多模型对比评测实战指南
llama-4_vs_deepseek-r1 是 ai-engineering-hub 中一个“以检索增强生成(RAG)为核心赛道的模型对比评测”实战项目:用同一套 LlamaIndex RAG 管道、同一份知识库与同一条评估链路,让 Groq 托管的 Llama 4 与 DeepSeek-R1 在同题竞答,再用开源可观测平台 Opik 的四个 RAG 指标完成量化对比。读完本文,你将掌握如何搭建支持模型热切换的 Streamlit 问答应用、如何用 LlamaIndex Workflow 编排 ingest→retrieve→synthesize 流水线,以及如何复用同一个评估数据集对两个模型做公平的离线评测。
一、项目概览:一条流水线,两套模型,同一把尺子
项目的核心思路非常清晰:让所有非模型因素保持一致,只让“大模型”成为唯一的变量。
- 检索与索引层统一使用 LlamaIndex(
VectorStoreIndex+FastEmbed本地嵌入模型); - 推理层统一通过 Groq 高速 API 调用两个参赛模型;
- 评测层统一使用 Opik 的 LLM-as-a-Judge 指标(幻觉、答案相关性、上下文精度、上下文召回);
- 评测语料固定为仓库内置的 Paul Graham 文章与 5 道问答对。
从 llama-4_vs_deepseek-r1/README.md 的说明看,这套工程由三个可独立运行的部分组成,对应目录结构如下:
llama-4_vs_deepseek-r1/
├── app.py # Streamlit 交互式 RAG 问答应用(在线对比)
├── workflow.py # LlamaIndex Workflow 事件驱动流水线
├── evaluation.ipynb # Opik 驱动的离线模型评测(评分核心)
├── pyproject.toml / uv.lock # uv 依赖锁定
├── assets/ # 界面所需的品牌 Logo
├── data/
│ └── DeepSeek.pdf # workflow.py 示例用的待索引文档
└── eval-data/
├── test.csv # 5 道带标准答案与上下文的问题集
└── paul_graham/
└── paul_graham_essay.txt # 评测知识库语料
一个直观的区分是:app.py 面向交互式人工对比(上传自己的 PDF 即可轮流提问两个模型),evaluation.ipynb 面向批量式自动评测(在固定语料上打分比较)。
二、环境准备与依赖同步
项目基于 uv 管理 Python 依赖,pyproject.toml 声明了完整的技术栈:
llama-index >= 0.12.28(核心框架)以及llama-index-llms-groq、llama-index-embeddings-fastembed、llama-index-utils-workflow、llama-index-embeddings-huggingface、llama-index-embeddings-instructor、llama-index-llms-ollama等扩展包(依赖清单见 pyproject.toml);opik >= 1.6.13(评测与可观测);streamlit >= 1.44.1(Web 界面);python-dotenv(环境变量加载,已被锁定在 uv.lock 中);- 要求
requires-python >= 3.12。
在项目目录下同步依赖:
uv sync
uv sync 会依据 pyproject.toml 与 uv.lock 创建虚拟环境并锁定安装全部依赖。若需要在 Notebook 内核中使用该环境,请选择 .venv 对应的 Python 内核(evaluation.ipynb 的元数据中即记录了 .venv 内核)。
三、环境变量配置:哪些 Key 是必需的
README 要求配置以下环境变量:
GROQ_API_KEY=...
OPENAI_API_KEY=...
两个变量职责不同:
| 变量 | 用途 | 缺失后果 |
|---|---|---|
GROQ_API_KEY |
调用 Llama 4 / DeepSeek-R1 两个参赛模型(RAG 生成阶段) | app.py 直接报错并 st.stop() |
OPENAI_API_KEY |
评估阶段充当“裁判/评判模型”以及 o1 相关场景 | 评估指标无法打分 |
README 明确提示:OpenAI API Key 是评估阶段需要的(“needed for using o1 and a judge during evaluation”)。在 evaluation.ipynb 中可以看到,opik.evaluation.evaluate() 的 experiment_config 里以 {"model": "gpt-3.5-turbo"} 指定了裁判模型——也就是由 OpenAI 模型去评判 Groq 两个候选模型的答案质量,因此两者缺一不可。
README 建议参照 .env.example 创建自己的 .env 文件。应用启动时,app.py 顶部会调用 load_dotenv()(app.py)自动读取项目根目录的 .env;evaluation.ipynb 同样在执行首段调用 load_dotenv()。另外在 app.py 的侧边栏中你也可以直接输入 Groq API Key,代码会优先读取会话中输入的值,其次才是环境变量(见 app.py):
api_key = st.session_state.get("groq_api_key", os.getenv("GROQ_API_KEY"))
四、双模型映射机制:一个“开关”切换参赛选手
项目在两个文件中用几乎相同的方式维护了“界面选项 → Groq 模型 ID”的映射关系。
在交互应用 app.py 中,通过下拉框选项选择模型:
@st.cache_resource
def load_llm(model_option):
if model_option == "Llama 4":
llm = Groq(model="meta-llama/llama-4-scout-17b-16e-instruct")
elif model_option == "DeepSeek-R1":
llm = Groq(model="deepseek-r1-distill-llama-70b")
return llm
return llm
在 workflow.py 中则收敛为一张字典,并通过 Settings.llm 注入全局配置:
GROQ_MODELS = {
"Llama 4": "meta-llama/llama-4-scout-17b-16e-instruct",
"DeepSeek-R1": "deepseek-r1-distill-llama-70b"
}
两个参赛模型的具体 Groq 模型标识如下:
| 参赛模型 | Groq 模型 ID | 说明 |
|---|---|---|
| Llama 4 | meta-llama/llama-4-scout-17b-16e-instruct |
Meta 的开源 Llama 4 系列 MoE 模型,ID 后缀 17b-16e 表明其稀疏专家结构 |
| DeepSeek-R1 | deepseek-r1-distill-llama-70b |
DeepSeek-R1 蒸馏到 Llama-70B 的推理模型,擅长逐步推理 |
注意两个文件中的下拉选项命名略有差异:app.py 使用 "Llama 4",evaluation.ipynb 使用 "Llama-4"(连字符),使用时需保持字符串与映射键一致,否则 workflow.py 会抛出 ValueError(见 workflow.py)。
五、在线对比:运行 Streamlit 交互式 RAG 应用
README 给出的启动命令非常简短:
streamlit run app.py
启动后浏览器会打开一个标题为 “Llama 4 vs DeepSeek-R1 RAG Battle” 的界面。整体交互链路如下。
5.1 侧边栏:填 Key、选模型、传 PDF
左侧边栏集中了三个核心操作(见 app.py):
- 输入 Groq API Key(密码框,若留空则回退到环境变量);
- 在下拉框中选择
Llama 4或DeepSeek-R1; - 通过文件上传控件选择
.pdf文档(type="pdf"限定文件类型)。
PDF 上传后,应用将其写入 tempfile.TemporaryDirectory() 临时目录,再交给 LlamaIndex 的 SimpleDirectoryReader 读取(见 app.py):
loader = SimpleDirectoryReader(
input_dir=temp_dir,
required_exts=[".pdf"],
recursive=True
)
docs = loader.load_data()
5.2 索引构建与 QA 提示词定制
索引用的是本地嵌入模型 + 内存向量索引,无需额外部署向量数据库(见 app.py):
embed_model = FastEmbedEmbedding(model_name="BAAI/bge-large-en-v1.5")
Settings.embed_model = embed_model
index = VectorStoreIndex.from_documents(docs, show_progress=True)
Settings.llm = llm
query_engine = index.as_query_engine(streaming=True)
qa_prompt_tmpl_str = (
"Context information is below.\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"Given the context information above I want you to think step by step to answer the query in a crisp manner, in 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}
)
值得注意的两处实现细节:
- 提示词内置“逐步思考 + 拒绝作答”约束:要求模型基于上下文 step-by-step 给出简洁回答,不知道就说
I don't know!,可有效抑制 RAG 场景下的幻觉。 - 提示词注入点:通过
update_prompts({"response_synthesizer:text_qa_template": ...})覆盖响应合成器的默认文本问答模板,这也是后续 Opik 评估中ContextRecall等指标得以成立的基础——模型必须先忠实引用上下文。
另外,嵌入模型用的是 BAAI/bge-large-en-v1.5,该模型会在首次索引时由 FastEmbed 自动下载。
5.3 会话缓存与流式对话
应用用 Streamlit session_state 做两级缓存:id 区分会话、file_cache 以 会话ID-文件名 为键缓存 query_engine 对象,避免同一文档反复重建索引(见 app.py 与 app.py)。
问答部分走流式输出:query_engine.query(prompt) 返回流式响应,主循环逐个读取 streaming_response.response_gen 的分块并实时渲染(见 app.py):
streaming_response = query_engine.query(prompt)
for chunk in streaming_response.response_gen:
full_response += chunk
message_placeholder.markdown(full_response + "▌")
界面右上角的 “Clear ↺” 按钮对应 reset_chat():清空消息记录与上下文缓存,并调用 gc.collect() 及时释放内存中的索引(见 app.py)。
在线对比的标准操作是:先选 Llama 4 上传文档提问并记录答案 → 点击 Clear → 切换到 DeepSeek-R1(同一份 PDF 会因缓存命中而跳过重建索引)→ 提出相同问题,从而直观感受两者在回答风格、推理深度上的差异。
六、流水线重构:用 LlamaIndex Workflow 编排 RAG
workflow.py 演示了用 LlamaIndex 新一代 Workflow API 将同一套 RAG 逻辑写成事件驱动流水线的做法。它继承 Workflow 基类,定义了一个携带检索结果的 RetrieverEvent:
class RetrieverEvent(Event):
"""Result of running retrieval"""
nodes: list[NodeWithScore]
三个 @step 装饰的步骤构成了完整的处理链(见 workflow.py):
| 步骤 | 输入事件 | 职责 | 关键调用 |
|---|---|---|---|
ingest |
StartEvent(dirname) |
从目录加载文档并建索引 | SimpleDirectoryReader(...) → VectorStoreIndex.from_documents() |
retrieve |
StartEvent(query, index) |
向量检索 Top-K 文档 | index.as_retriever(similarity_top_k=2) → aretrieve(query) |
synthesize |
RetrieverEvent(nodes) |
基于检索节点生成流式答案 | CompactAndRefine(streaming=True) → asynthesize() |
关键点在于检索与索引对两个模型完全共享:无论最终选择 Llama 4 还是 DeepSeek-R1,RAGWorkflow 只在初始化时通过 GROQ_MODELS 决定 Groq 实例,retrieve 与 synthesize 两步骤的逻辑完全一致,从源码结构可以推断这正是为了保证“对比只看模型差异”。
其中 retrieve 步骤还通过 await ctx.set("query", query) 把查询词写入 Workflow 上下文,供后续 synthesize 用 ctx.get("query") 取回——这是 LlamaIndex Workflow 在步骤间传递数据的标准用法。
文件末尾的 main() 给出了端到端示例:初始化 Llama 4 工作流 → ingest_documents("data") 索引 data/DeepSeek.pdf → 提问 "How was DeepSeekR1 trained?" 并逐块流式打印答案(见 workflow.py):
python workflow.py
七、离线评测:用 Opik 给两个模型打 RAG 分数
在线问答只能给人看“体感”,真正可复现的结论要靠 evaluation.ipynb 这套离线评测。评测流程分为五步。
7.1 配置 Opik 与接入 LlamaIndex 追踪
首先初始化 Opik 并接入 LlamaIndex 的回调机制,让每次 RAG 查询自动上报成可观测 Trace:
import opik
opik.configure(use_local=False) # 关闭本地模式,上报至 Opik 云端项目
from llama_index.core import Settings
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 的检索、合成等操作全部记录到 Opik,方便事后在 Trace 视图里排查“是检索没召回,还是模型答错”。
7.2 建立评测数据集
评测数据来自 eval-data/test.csv,它包含三列:Question(问题)、Answer(标准答案)、Context(支持答案的原文片段)。Notebook 把 CSV 转成 Opik 数据集所需的 input / expected_output / context 结构:
client = Opik()
dataset = client.get_or_create_dataset(name="Test dataset")
df = pd.read_csv("./eval-data/test.csv")
qa_pairs = [
{"input": row["Question"], "expected_output": row["Answer"], "context": row["Context"]}
for _, row in df.iterrows()
]
# dataset.insert(qa_pairs) # 首次创建数据集时取消注释
其中一条样例数据(内容为 Paul Graham 自传的阅读理解):
{
'input': 'What was the very first programming language Paul Graham used when he began learning to program on the IBM 1401?',
'expected_output': 'He used an early version of Fortran on the IBM 1401.',
'context': 'The language we used was an early version of Fortran. ...'
}
知识库语料则是 eval-data/paul_graham/paul_graham_essay.txt(Paul Graham 长文 “What I Worked On”)。注意:评测阶段的嵌入模型换成了 nomic-ai/nomic-embed-text-v1,与 app.py 在线应用的 BAAI/bge-large-en-v1.5 不同——这是为了说明“评测环境与线上环境可以各自独立调优”,但在对比两模型时,嵌入与语料必须全程一致才能保证公平。
7.3 封装被测任务并切换参赛模型
被测对象被包装成 Opik 可调度的 evaluation_task,其中用 @track 装饰以生成独立 Trace:
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'])}
而“谁上场”由 model_name 决定:
model_name = 'Llama-4'
# model_name = 'DeepSeek-R1' # 换人时切换这行
llm = load_llm(model_name) # Groq(...) 封装
Settings.llm = llm
这意味着跑完全部 5 道题后,把 model_name 改成另一个模型再执行一次 evaluate(),就会得到两份使用完全相同数据集、相同指标、不同候选模型的实验结果,二者可直接对齐比较。
7.4 定义评分指标并执行
评测使用 Opik 提供的四个经典 RAG 指标(见 opik.evaluation.metrics):
from opik.evaluation.metrics import (
Hallucination, # 幻觉:答案是否偏离给定上下文
AnswerRelevance, # 答案相关性:是否切题
ContextPrecision, # 上下文精度:检索片段中相关部分占比
ContextRecall # 上下文召回:标准答案所需信息是否被检索到
)
执行评测时,把四个指标一次性传入,并用 experiment_config 指定裁判模型为 GPT-3.5-turbo:
from opik.evaluation import evaluate
evaluation = evaluate(
dataset=dataset,
task=evaluation_task,
experiment_name=model_name, # 用模型名区分两场实验
scoring_metrics=[hallucination_metric, answer_relevance_metric,
context_precision_metric, context_recall_metric],
experiment_config={"model": "gpt-3.5-turbo"}
)
实验结束后可在 Opik 平台对比两场 experiment 的指标均值,形成类似“Llama-4 幻觉率 vs DeepSeek-R1 幻觉率”的可量化结论。
7.5 限流与重试:一个来自 Notebook 的真实提醒
evaluation.ipynb 中完整保留了实际运行遇到的 Groq 429 Rate Limit 错误记录:评测 Llama 4 时提示 tokens per minute (TPM): Limit 6000, Used 11451,多次重试后仍失败。这提供了一个非常真实的生产经验:
- Opik 的评测默认用线程池并发执行任务(代码路径为
ThreadPoolExecutor+task_threads参数),多个请求同时涌向 Groq 很容易撞上 TPM 限额; - Opik 检测到限流后会提示:“We recommend reducing the amount of parallel requests by setting
task_threadsevaluation parameter to a smaller number”; - 因此,执行本评测时应控制并发度(例如在
evaluate()中显式调小task_threads),并为上游 API 预留足够的 token 额度,必要时对两个模型分批串行评测。
八、评测公平性与注意事项总结
把这套“模型对比”工程跑出可信结论,需要注意几点:
- 变量隔离:切换模型时,索引、嵌入模型、语料、测试集与评分指标一律保持不变,只改
load_llm/GROQ_MODELS中映射的模型 ID; - 双 Key 就绪:
GROQ_API_KEY负责生成候选回答,OPENAI_API_KEY供 Opik 的裁判模型打分,缺一不可; .env正确加载:确保在项目根目录运行(load_dotenv()默认查找当前目录的.env),或直接在 Streamlit 侧边栏输入 Key;- 规避限流:降低
task_threads、错峰运行,避免 429 中断整场评测; - 入口区分:想要人工体验上传文档问答用
streamlit run app.py;想要批量量化打分则在 Jupyter 中顺序跑通evaluation.ipynb并为两个模型各执行一次evaluate();想要复用事件驱动架构则在 workflow.py 基础上扩展自己的步骤。
# 一键复现三件套
uv sync # 1. 同步依赖
streamlit run app.py # 2. 在线双模型问答(需 GROQ_API_KEY)
python workflow.py # 3. 流水线示例(索引 data/DeepSeek.pdf 后提问)
通过这套工程,你既可以把任意“文档问答 + 模型选型”的需求落地为可运行应用,也能把它沉淀成可重复执行的评测基线——这正是 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 StartedRust0631
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证件照制作算法。Python09
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