🚀 重现 Search-R1 之旅

社区文章 发布于2025年6月3日

申承允 · 2025

我刚刚完成了一个深入的项目,旨在重现并略微超越原始的 Search-R1 检索增强方法。在此过程中,我发现了一个关键见解:**使用反思性推理短语(例如“再想想”、“重新验证”)与基准性能的提高呈强比例关系**。以下是我的旅程 👇


🛠 我做了什么

  • VERL 内部,使用 **GRPO** 对 **Qwen 2.5-3B-Instruct** 在 **维基百科语料库** 上进行了 RL 调优。
  • 实施并跟踪了反思性推理短语(例如“再想想”、“重新验证”)的使用情况。
  • **在 Weights & Biases 中记录了每一次运行,以实现完全透明。** wandb 报告

📊 结果 (Pass@1)

数据集 论文 (Search-R1, Qwen2.5-3B-it) 本次运行
NQ 39.7 % 40.6 %
TriviaQA 56.5 % 58.2 %
PopQA 39.1 % 42.0 %
HotpotQA 33.1 % 33.8 %
2Wiki 31.0 % 33.2 %
Musique 12.4 % 11.1 %
Bamboogle 23.2 % 29.6 %

检索到的段落**从损失中被屏蔽**,因此模型学习何时搜索而不是记忆答案。

**这项工作**的平均得分达到 0.349,比 RAG 基线(0.270)提高了约 **1.29 倍**。值得注意的是,它也略优于原始的 search-r1 模型。


🧠 推理风格

<think><tool_call><tool_response><answer>


✅ 反思行为

image/png

**反思短语**(例如“再想想”、“重新验证”、“让我们重新考虑”)在训练过程中逐渐增加使用频率。此图比较了 7 个基准(**NQ、TriviaQA、PopQA、Musique、HotpotQA、Bamboogle 和 2wikimultihopqa**)的平均性能得分与反思短语比率。值得注意的是,反思短语比率与平均得分显示出**强烈的正向关系,表明更多的反思性语言对应着更好的基准性能**。

image/png

你可以观察到诸如**“经仔细检查”**和**“再想想”**等自我验证语句,显示了模型在回答前重新评估自身主张的内在倾向。


💻 完整推理脚本(放在权重文件旁并运行)

#!/usr/bin/env python3
"""
Minimal multi-turn tool-calling demo for the Qwen2.5-3B Search-R1 model
"""
from __future__ import annotations
import json, re, sys
from typing import List
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from duckduckgo_search import DDGS

SYS = "You are a helpful and harmless assistant."
USR_PRE = ("Answer the given question. You must conduct reasoning inside <think> and "
           "</think> first every time you get new information. After reasoning, if you "
           "lack knowledge, call a search engine by <tool_call> query </tool_call>. "
           "It returns results between <tool_response> and </tool_response>. "
           "Provide the final answer inside <answer>. For example, <answer> Beijing </answer>. Question: ")

MODEL = "Seungyoun/qwen2.5-3b-it_searchR1-like-multiturn"
MAX_TURNS, MAX_NEW = 4, 512
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

SEARCH_SCHEMA = {
    "type": "function",
    "function": {
        "name": "search",
        "description": "DuckDuckGo web search",
        "parameters": {
            "type": "object",
            "properties": {
                "query_list": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Fully-formed semantic queries."
                }
            },
            "required": ["query_list"],
        },
    },
}

def prompt(q: str) -> List[dict]:
    return [{"role": "system", "content": SYS},
            {"role": "user", "content": USR_PRE + q}]

def ddg(q: str, k: int = 5) -> str:
    with DDGS() as ddgs:
        hits = list(ddgs.text(q, safesearch="moderate", max_results=k))
    return "\n".join(f"{i+1}. {h['title']}{h['body']} ({h['href']})" for i, h in enumerate(hits))

def parse_queries(raw: str) -> List[str]:
    try:
        p = json.loads(raw)
        if p.get("name") == "search":
            return p.get("arguments", {}).get("query_list", [])
    except json.JSONDecodeError:
        pass
    return [raw]

def main() -> None:
    q = sys.argv[1] if len(sys.argv) > 1 else "How is the weather in Seoul?"
    tok = AutoTokenizer.from_pretrained(MODEL, padding_side="left")
    model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype=torch.bfloat16, device_map="auto")
    hist = tok.apply_chat_template(prompt(q), tools=[SEARCH_SCHEMA], add_generation_prompt=True, tokenize=False)
    pat = re.compile(r"<tool_call>\s*(.*?)\s*</tool_call>", re.S)

    for turn in range(MAX_TURNS):
        enc = tok(hist, return_tensors="pt").to(DEVICE)
        out = model.generate(**enc, max_new_tokens=MAX_NEW, temperature=0.7, do_sample=True)
        delta = tok.decode(out[0][enc.input_ids.shape[1]:], skip_special_tokens=True)
        print(f"\n===== Assistant (turn {turn+1}) =====\n{delta}\n")
        hist += delta
        m = pat.search(delta)
        if not m: break
        results = "\n---\n".join(ddg(s, 5) for s in parse_queries(m.group(1)))
        hist += f"<tool_response>\n{results}\n</tool_response>"

if __name__ == "__main__":
    main()

这将得到

image/png


🔗 亲自尝试

权重**+**脚本:https://huggingface.co/Seungyoun/qwen2.5-3b-it_searchR1-like-multiturn


🙏 致谢

Qwen 团队、Search-R1 作者、VERL 维护者以及使重现成为可能的所有开源社区。

社区

注册登录 发表评论