从 RAG 到 Agent:跑通两个 Demo 后,我终于分清了这两个词

0 阅读5分钟

最近接连跑通了两个 LLM 应用的小 Demo:先是一个 RAG 私有文档问答,然后是一个 ReAct 模式的旅行助手 Agent。最大的收获不是代码本身,而是终于搞明白了一件天天被提但一直模模糊糊的事——RAG 和 Agent 到底差在哪

第一站:RAG——让模型"知道"我的文档

第一个 Demo 很经典:让 LLM 回答它训练数据里不可能有的问题——我自己的笔记。

流程两条管线:

# 建索引:markdown 文档 → 切成 500 字的块 → 算 embedding → 存进 Chroma
chunks = chunk_text(text, size=500, overlap=50)
embeddings = embedder.encode(chunks)
collection.add(ids=..., documents=chunks, embeddings=embeddings)
# 查询:问题 → 向量检索 top3 → 拼进 prompt → 调一次 LLM 生成回答
def answer(question: str) -> str:
    chunks = retrieve(question)          # 检索最相似的 3 块
    prompt = PROMPT_TEMPLATE.format(context=..., question=question)
    response = CLIENT.chat.completions.create(...)
    return response.choices[0].message.content

跑通之后回头看,RAG 的特点非常明显:

  • 流程是代码写死的:检索 → 拼 prompt → 生成,永远这三步,一步不会多
  • LLM 只被调用一次,它自始至终不知道"检索"这件事的存在
  • LLM 的角色是拿着参考资料的答题者——你喂它三段资料,它负责好好回答,答不出就说"文档里没说"

一句话:RAG 解决的是"知道"的问题——把模型不知道的私有知识,现场塞给它。

第二站:Agent——让模型"做事"

第二个 Demo 是一个旅行助手,任务一句话:查北京今天的天气,然后根据天气推荐景点

注意这是个两步任务,而且第二步依赖第一步的结果。如果是普通程序,我们得自己写"先查天气、再搜景点"的调度逻辑。但 Agent 的做法是:只给模型工具,让模型自己决定怎么串

工具就是两个普通 Python 函数,没有任何特殊之处:

def get_weather(city: str) -> str:
    data = requests.get(f"https://wttr.in/{city}?format=j1", timeout=10).json()
    cur = data["current_condition"][0]
    return f"{city}当前天气:{cur['weatherDesc'][0]['value']},气温{cur['temp_C']}摄氏度"def get_attraction(city: str, weather: str) -> str:
    result = tavily.search(query=f"{city} 适合{weather}天气的旅游景点推荐", max_results=3)
    return "根据搜索,为您找到以下信息:" + ";".join(r["title"] for r in result["results"])

然后在 System Prompt 里告诉模型有哪些工具、按什么格式说话(Thought / Action / Observation 协议),核心循环长这样:

for i in range(1, MAX_LOOPS + 1):
    # ① 问模型下一步干嘛
    reply = llm.chat.completions.create(model=MODEL, messages=messages).choices[0].message.content
​
    # ② 模型说"信息够了" → 结束
    if "Final Answer" in reply:
        return reply.split("Final Answer:")[-1]
​
    # ③ 抠出工具名和参数,真正执行
    tool_name, args = parse_action(reply)
    observation = TOOLS[tool_name](**args)
​
    # ④ 关键:把工具结果拼成 Observation 喂回对话,进入下一轮
    messages.append({"role": "assistant", "content": reply})
    messages.append({"role": "user", "content": f"Observation: {observation}"})

实际跑起来的输出(真实运行结果节选):

--- 循环 1 ---
Thought: 用户需要先查询北京今天的天气……第一步先调用天气查询工具。
Action: get_weather(city="北京")
Observation: 北京当前天气:Sunny,气温27摄氏度
​
--- 循环 2 ---
Thought: 北京今天是晴天,气温27摄氏度,适合户外活动。接下来根据这个天气条件调用景点推荐工具。
Action: get_attraction(city="北京", weather="Sunny")
Observation: 根据搜索,为您找到以下信息:北京在sunny天气下最值得去的旅游景点推荐及理由;……
​
--- 循环 3 ---
Thought: 工具返回了景点推荐结果……
Final Answer: 北京今天天气晴朗,气温约27°C,非常适合户外游玩……

执行结果: image.png

看循环 2 那行 Action:weather="Sunny" 这个参数是模型自己填的——它把循环 1 拿到的天气结果,当作循环 2 的输入传了进去。代码里没有任何一行写"先查天气再搜景点",整个流程的编排完全发生在模型的"脑子"里。

这就是 Agent 的本质:LLM 不只是答题者,而是指挥官,工具是它的手脚,那个"问模型 → 干活 → 把结果给模型看"的循环就是它的心脏。

区别,一张表说清

RAGAgent
LLM 被调几次1 次N 次(每轮循环一次)
流程谁定的代码写死LLM 运行时自己决定
LLM 的角色拿着参考资料的答题者调度工具的指挥官
解决什么问题知道:掌握私有知识做事:完成多步任务
能否根据中间结果调整不能,管线是直的能,下一步取决于上一步

我自己总结的最快判断法:看有没有"循环 + 决策" 。没有循环、模型只出场一次的,是 RAG;有循环、每一步"接下来干嘛"由模型现场判断的,是 Agent。

顺手踩的一个坑

用正则解析模型的 Action: get_weather(city="北京") 后,我最初想用 ast.literal_eval 把参数转成字典——结果直接报 SyntaxError。原因:literal_eval 只认 Python 字典写法 {"city": "北京"},而模型输出的是函数调用写法 city="北京"。最后老老实实用正则逐对提取:

args = dict(re.findall(r'(\w+)="([^"]*)"', args_str))  # city="北京" → {"city": "北京"}

教训:模型的输出是"协议",解析器必须和 prompt 里约定的格式严丝合缝,差一点都跑不通。

最后

两件事并不冲突:完全可以把"检索知识库"包装成一个工具,放进 Agent 的工具箱——那时 RAG 就成了 Agent 的其中一只手,这也是现在很多落地系统的真实架构(Agent 负责决策,RAG 负责供料)。

下一步计划:给这个 Agent 加第三个工具,试试让它在更多工具里自己选择。


环境:Python 3.12 + DeepSeek(大脑)+ wttr.in(天气)+ Tavily(搜索),全文代码不足 100 行。