上个月我接了个需求:做一个能帮用户查产品手册的客服助手。需求看起来平平无奇对吧?但客户提了个要命的条件——用户问完产品参数后,助手得能直接替用户去查库存、比价格、下订单。

光靠RAG查文档是搞不定的。Agent虽然能做动作,但没有实时知识又容易胡说八道。

折腾了两周,最后搞了个 RAG + Agent 的混合架构。今天聊聊这玩意怎么落地。


为什么光有RAG不够,光有Agent也不行

先说RAG。RAG说白了就是"你先去查资料,查到以后让大模型根据资料回答"。优点是答案靠谱,不会瞎编。缺点是——它只能回答,不能做事。

你问"帮我查一下这款产品的库存",RAG能回答"根据手册,型号XYZ-100的库存查询接口是/check-stock"。

但它不会帮你调接口。

Agent反过来。它能做事——调API、写文件、发邮件——但它靠的是大模型自身的知识。大模型知道"调库存接口"这个动作,但不知道你们公司接口的具体参数是什么,Token里什么格式,权限怎么签。

所以RAG做不了动作,Agent没有实时知识。

把两者结合起来,就成了。


核心架构:RAG给知识,Agent做决策

我最后搭的架构分三层。

第一层:路由层

用户提问进来,先过一个小模型判断意图。是"知识问答类"还是"操作执行类"?

  • 知识问答类 → 直接走 RAG 查询,省Token
  • 操作执行类 → 走 Agent 链路
  • 混合类(问产品后要下单) → 先RAG查,再Agent执行

这个路由用GPT-4o-mini就够了,又快又便宜。我实测准确率大概87%,剩下的13%误判走错了也不致命,最多多花点Token。

def route_intent(query: str) -> str:
    """判断用户意图"""
    prompt = f"""分类用户意图:
A - 知识问答(只需查资料回答)
B - 操作执行(需要调用外部工具/API)
C - 混合(先查知识再执行操作)

用户问题:{query}

只返回 A/B/C,不要多余内容。"""
    
    result = llm_call(prompt, model="gpt-4o-mini")
    return result.strip()
第二层:RAG知识层

这里的RAG不是传统RAG。传统RAG存的是文档段落,我存的是"工具元数据"。

什么意思呢?就是把每个API接口的描述、参数、返回格式、使用场景——当作文档片段索引起来。

# 存储到向量库的工具元数据结构
tool_doc = {
    "tool_name": "check_stock",
    "description": "查询产品实时库存",
    "parameters": {
        "product_id": "string, 产品ID",
        "warehouse": "string, 可选,仓库编码"
    },
    "return_format": "json { quantity: int, status: string }",
    "usage_scenarios": ["查询库存", "下单前校验"],
    "auth_required": True,
    "api_endpoint": "https://api.company.com/v1/stock"
}

这样当Agent需要决定调用什么工具时,先通过RAG检索出最相关的工具元数据,再决定调哪个。

这比把所有工具写在System Prompt里靠谱得多。我试过塞40个工具的System Prompt,模型直接傻了——不是记不住参数,就是混着调用。

第三层:Agent执行层

Agent部分不复杂,就是 OpenAI Function Calling 或者 Anthropic Tool Use。

关键区别在于:工具列表不是写死的,而是从RAG检索结果动态生成的。

def build_tools_from_rag(query: str, k: int = 3):
    """根据用户问题,从RAG中检索最相关的工具"""
    results = vector_db.search(query, k=k)
    tools = []
    for r in results:
        tool = {
            "type": "function",
            "function": {
                "name": r["tool_name"],
                "description": r["description"],
                "parameters": {
                    "type": "object",
                    "properties": {
                        k: {"type": "string", "description": v}
                        for k, v in r["parameters"].items()
                    }
                }
            }
        }
        tools.append(tool)
    return tools

这么搞的好处是:理论上支持无限多个工具。实际跑下来,一次检索3-5个工具,模型处理得游刃有余。


踩坑记录:三个让我头疼的问题

坑1:RAG检索到不相关工具,Agent硬要用

RAG检索工具时,如果检索精度不够,Agent可能会用错工具。比如用户说"帮我查库存",RAG检索到了"check_stock"和"create_order",Agent莫名其妙就跑去下单了。

解决办法:给每个工具加一个"relevance_score"字段。检索后过滤掉相关性低于阈值的结果。我设为0.75,低于这个值的不传入Agent。

坑2:上下文窗口膨胀

每次Agent执行前都要塞进3-5个工具描述,每个描述300-500字。多轮对话下来,Context越来越长。

解决办法:只保留当前轮次检索到的工具。上一轮调过的工具,只保留调用结果,不保留工具描述。这样每轮新增的Token量是固定的。

坑3:工具调用失败后的知识回退

Agent调用API失败了——比如库存接口挂了。如果Agent说"我调了,但报错了",用户一脸懵逼。

解决办法:加一个fallback机制。当Agent工具调用失败时,自动触发RAG查询对应工具的"常见问题"和"故障处理"文档,把解决方案返回给Agent,让它重新尝试。

def tool_call_with_fallback(tool_name: str, params: dict):
    try:
        return call_api(tool_name, params)
    except APIError as e:
        # 查询该工具的故障处理知识
        fallback_knowledge = rag_retrieve(
            f"{tool_name} 失败 常见问题"
        )
        return {
            "status": "failed",
            "error": str(e),
            "fallback_advice": fallback_knowledge
        }

实际效果:真香

这套架构跑了一个多月了,处理了大概1200多真实查询。

  • 纯RAG能覆盖的:约40%(直接问知识类问题)
  • 纯Agent能搞定的:约25%(简单操作类)
  • 需要RAG+Agent配合的:约35%(先查再操作)

整体准确率从之前纯Agent方案的76%提升到了91%。多数错误出现在路由误判上,把混合类判成了纯知识类。

对于经常被问到的场景,我还加了缓存——同样的Query直接用缓存结果,响应时间从3-5秒降到了500ms以内。


写在最后

RAG和Agent不是二选一的关系。RAG负责让AI知道"这个世界是什么样的",Agent负责"让AI去改变这个世界"。两个缺一个,应用都跑不通。

如果你也在搭类似的系统,我建议从简单的路由+RAG开始,慢慢加Agent能力。别一上来就想搞全自动Multi-Agent——我见过太多团队死在第一步。

下一篇聊聊这个架构里怎么设计Agent的短期记忆和长期记忆,欢迎关注。

有问题评论区聊。


参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐