引入RAG

🧠 为什么需要 RAG?(给 AI 装上"私域大脑")

目前的 Tools 节点里,我们只有一个 search_web(Tavily)。它就像是让 AI 去百度/谷歌查资料。

但想象一下这个真实的痛点:当你需要调用一些极其冷门的接口 ,或者进行类似 Siemens NX (UG) 这类重型工业软件的二次开发时,最核心的 API 手册、SDK 说明文档往往是本地的 PDF、Markdown 或者是内部 Wiki,公网上的资料非常稀缺甚至已经过时。此时 Tavily 根本搜不到任何有用的代码片段,AI 就会开始"胡说八道"(产生幻觉)

解决方案 :我们要给 AI 开发第二个工具------search_local_docs(本地文档检索工具)。

当 AI 发现需求属于私有领域时,它会自动调用这个工具,去翻阅你提前"喂"给它的本地离线文档。


🛠️ RAG 技术架构拆解(我们要学什么?)

要让大模型"看懂"本地文件,不是直接把几万字的文档全塞给它(上下文会爆,而且极其昂贵),而是要经过四个极其优雅的步骤:

  1. 加载与切分 (Load & Split):把长文档按段落切成一块块的"文本块(Chunks)"。
  2. 向量化 (Embedding):用一种特殊的 AI 模型,把每一块文字翻译成一长串数字(高维向量)。意思相近的段落,数字也会很接近。
  3. 存入向量数据库 (Vector DB):把这些数字存进一个专门的数据库(如 ChromaDB)中。
  4. 语义检索 (Retrieve):当 AI 调用工具提问时,我们把问题也变成向量,去数据库里做"距离比对",把最相关的 3 个文本块拿出来,喂给大模型。

🚀 第一步:准备基建与环境

今天我们要手搓一个本地的向量数据库,并且不需要调用外部 API 就能完成向量化转换(完全免费和私密)。

1. 安装核心依赖包:

打开你的虚拟环境终端,运行以下命令。我们需要安装 LangChain 的社区版组件、本地向量数据库 Chroma,以及本地的 Embedding 模型工具包:

复制代码
pip install langchain-community chromadb sentence-transformers

(注:安装 sentence-transformers**可能会连带安装 PyTorch,文件稍大,需要耐心等待几分钟。)

2. 准备一个"私有秘籍"文档:

在你的项目文件夹下,新建一个文本文件,命名为 secret_api_doc.txt

我们在里面随便伪造一些极其冷门、网上绝对搜不到的假 API 文档,用来测试 AI 能不能真的从本地学到知识。

把下面这段内容复制进 secret_api_doc.txt

复制代码
# 王氏虚构开发框架 (Wang-Framework) V1.0 API 手册

## 1. 核心网络模块
如果你需要建立一个高并发的底层连接,请不要使用标准的 socket。你应该使用 `WangNet.create_super_link()`。
参数说明:
- host (str): 目标地址
- threads (int): 并发线程数,必须大于等于 10,否则会抛出 LowThreadError。

示例代码:
import WangNet
link = WangNet.create_super_link(host="127.0.0.1", threads=20)

## 2. 图像魔法处理模块
如果需要对图像进行灰度化处理,本框架提供了一个极速接口:`WangImage.gray_magic(image_path)`。
注意:该接口处理完后,必须手动调用 `WangImage.flush_memory()`,否则会导致内存泄漏。

🛠️ 架构大升级:引入最强的切肉刀

在真实的 RAG(检索增强生成)工程中,我们几乎从来不用 CharacterTextSplitter,而是用它的终极进化版:RecursiveCharacterTextSplitter**(递归字符文本切分器)**。

它非常聪明,它会先尝试按段落切(双换行);如果段落太长,它会自动降级按句子切(句号);如果句子还长,再按单词切。绝不会出现切不断的情况!

为了消除上面的所有警告,并精准切分文本,我们需要做两步:

第一步:安装最新的包(解决弃用警告)

在终端里运行:

复制代码
pip install -U langchain-huggingface

第二步:替换完美的 play_rag.py****代码

把你的 play_rag.py 里的代码完整替换成下面这段(注意看导入包和切分器的变化):

复制代码
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter  # ✨ 换成最智能的切分器
from langchain_huggingface import HuggingFaceEmbeddings            # ✨ 使用最新官方推荐的包
from langchain_community.vectorstores import Chroma

print("1. 正在加载本地绝密文档...")
loader = TextLoader("secret_api_doc.txt", encoding="utf-8")
documents = loader.load()

# ✨ 工业级 RAG 标准切分配置
# chunk_size=150: 每块肉大概 150 个字符
# chunk_overlap=20: 每块肉之间保留 20 个字符的重叠(防止一句话被从中间硬生生切断,保留上下文语义)
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=150, 
    chunk_overlap=20
)
docs = text_splitter.split_documents(documents)
print(f"   [成功] 聪明的切刀把文档切成了 {len(docs)} 个独立知识块!")

print("2. 正在启动嵌入 AI (坐标系翻译官)...")
# 将原本的 "all-MiniLM-L6-v2" 替换为支持 50 多种语言的 "paraphrase-multilingual-MiniLM-L12-v2"
embedding_model = HuggingFaceEmbeddings(model_name="paraphrase-multilingual-MiniLM-L12-v2")

print("3. 正在将文字转化为高维向量并存入 Chroma 数据库...")
vector_db = Chroma.from_documents(documents=docs, embedding=embedding_model)

print("\n4. 引擎轰鸣:开始语义检索!")
print("-" * 40)
query = "我想把彩色图片弄成黑白的,另外搞完之后电脑会不会越来越卡啊?"
print(f"👤 你的大白话提问:{query}\n")

# k=1 表示只取距离最近的 1 个结果
results = vector_db.similarity_search(query, k=1)

print("🤖 数据库精准匹配到的私有知识片段:")
print(results[0].page_content)
print("-" * 40)

为了响应你之前提到的**"不要浪费内存和系统资源",在把这段代码封入主架构时,我们需要进行一个架构级的性能优化**。

🧠 逻辑思考:如何优雅地把 RAG 塞进工具箱?

如果我们每次调用工具时,都去重新读取 TXT 文件、重新切分、重新加载 Embedding 模型,那会极其消耗 CPU 和内存。

正确的工业级做法是:使用全局单例(Singleton)模式。 在整个 Python 进程启动时,我们只建一次库。以后大模型每次调工具,只负责"查",绝不重新"建"。


🛠️ 实战融合:打造 search_local_docs 工具

请打开你最新的主程序文件(比如 test9.py)。找到你定义工具(Tools)的地方(就在定义 search_web 的下面),按照以下步骤将代码补充进去:

第一步:在文件顶部增加必要的导包
复制代码
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
第二步:全局初始化本地知识库(紧贴在 search_web 定义的下方)

将下面这段代码粘进去。这段代码会在引擎启动时执行一次,把知识固化在内存里。

复制代码
# ==========================================
# 本地私有知识库初始化 (全局单例,极致节省资源)
# ==========================================
print(">>> [系统准备]: 正在初始化本地私有知识库 (Wang-Framework)...")
try:
    _loader = TextLoader("secret_api_doc.txt", encoding="utf-8")
    _text_splitter = RecursiveCharacterTextSplitter(chunk_size=150, chunk_overlap=20)
    _docs = _text_splitter.split_documents(_loader.load())
    _embedding_model = HuggingFaceEmbeddings(model_name="paraphrase-multilingual-MiniLM-L12-v2")
    # 构建基于内存的向量索引
    _vector_db = Chroma.from_documents(documents=_docs, embedding=_embedding_model)
    print(">>> [系统准备]: 本地私有知识库加载完成!")
except Exception as e:
    print(f">>> [系统警告]: 本地知识库加载失败,请检查 secret_api_doc.txt 是否存在。错误: {e}")
    _vector_db = None
第三步:使用 @tool 封装检索函数

紧接着上面的代码,定义我们的大模型专用工具:

复制代码
@tool
def search_local_docs(query: str) -> str:
    """
    当用户要求进行 Wang-Framework 二次开发、调用王氏虚构框架,
    或者处理高并发超级连接、图像魔法处理时,【必须】使用此工具检索本地的私有 API 手册。
    注意:不要用公网搜索去查 Wang-Framework,网上的都是错的!
    """
    if _vector_db is None:
        return "错误:本地知识库未正确加载,无法检索。"

    print(f"    [Tools] 触发本地知识库检索,关键词: {query}")
    # k=2 表示我们提取最相关的两段肉(防止一段没说全)
    results = _vector_db.similarity_search(query, k=2)

    if not results:
        return "本地文档中没有找到相关信息。"

    # 将查到的内容拼接成字符串返回给大模型
    docs_text = "\n\n".join([f"--- 本地片段 {i+1} ---\n{res.page_content}" for i, res in enumerate(results)])
    return f"找到以下本地私有 API 参考资料,请严格按照此资料编写代码:\n{docs_text}"
第四步:把新武器挂载到 AI 身上

找到定义 tools 列表的那一行,把新的工具加进去:

复制代码
# 将真实工具放入工具箱 (现在它是双持武器了!)
tools = [search_web, search_local_docs]

🛠️ 一、修改 Planner 节点:加装"智能阀门"

逻辑思考:

大文件直接全量检索会导致 Token 爆炸和噪音污染。我们通过定义 trigger_keywords(意图路由),只有当需求涉密时才开启检索。并且设定 score > 0.5 的安全及格线,严防死守不相关的废话进入架构师的大脑。

请将原有的 planner_node 替换为:

复制代码
def planner_node(state: SkillsCreatorState) -> dict:
    """架构师节点:带有智能阀门和阈值过滤的前置检索"""
    print("\n>>> [节点执行]: 进入 Planner 节点 (架构设计)")
    user_req = state.get("user_requirement")

    context_str = "无本地特殊框架参考资料。"

    # 阀门 1:意图路由 (Intent Routing)
    trigger_keywords = ["王氏", "Wang", "高并发", "魔法处理", "底座", "私有"]
    need_search = any(keyword in user_req for keyword in trigger_keywords)

    # 声明使用全局的 _vector_db
    global _vector_db 
    if _vector_db is not None and need_search:
        print("    [Planner] 触发私有领域关键词,正在大容量知识库中进行精准检索...")
        try:
            # 阀门 2:带分数阈值的检索
            results_with_scores = _vector_db.similarity_search_with_relevance_scores(user_req, k=2)
            valid_docs = [doc.page_content for doc, score in results_with_scores if score > 0.5]

            if valid_docs:
                context_str = "\n".join([f"- {content}" for content in valid_docs])
                print(f"    [Planner] 成功过滤出 {len(valid_docs)} 条高质量私有知识!")
            else:
                print("    [Planner] 知识库中未匹配到高相关度内容,放弃注入噪音。")
        except Exception as e:
            print(f"    [Planner 警告] 阈值检索暂不支持,降级为基础检索: {e}")
            docs = _vector_db.similarity_search(user_req, k=1)
            context_str = docs[0].page_content if docs else "无"

    prompt = ChatPromptTemplate.from_messages([
        ("system", """你是一个资深的 Python 架构师。请将需求拆解为清晰的开发步骤。不要写代码。
        【最高防御指令】:如果用户的需求涉及特定的私有框架(如王氏开发框架),请【务必】参考以下本地文档片段来设计步骤,绝对不允许瞎猜第三方库(如 Pillow, OpenCV 等)!
        
        === 本地参考文档 ===
        {context}
        ====================
        """),
        ("human", "需求:{user_req}")
    ])

    try:
        response: PlannerOutputSchema = (prompt | planner_llm).invoke({
            "user_req": user_req, 
            "context": context_str
        })
        print(f"    [Planner] 拆解了 {len(response.steps)} 个步骤。")
        return {"plan": response.steps}
    except Exception as e:
        print(f"    [Planner 警告] 需求拆解失败(可能触发安全拦截): {e}")
        return {"plan": ["分析用户需求并检索所需库的最新用法", "编写核心业务代码", "编写单元测试"]}

🛠️ 二、修改 Coder 节点:赋予"独立思考"与"依赖申报"权

逻辑思考:

架构师依然有可能犯错,必须赋予程序员"拒绝盲从"的底气。同时,为了让沙盒知道该下载什么库,强制要求 Coder 使用 <dependencies> 标签申报所需的第三方包。

请将原有的 coder_node 替换为:

复制代码
def coder_node(state: SkillsCreatorState) -> dict:
    """全面升级的 Coder 节点:融合对话、规划、报错、工具调用与依赖申报"""
    print("\n>>> [节点执行]: 进入 Coder 节点 (思考/调用工具/写代码)")

    user_req = state.get("user_requirement")
    plan = state.get("plan", [])
    error_message = state.get("error_message")
    count = state.get("iteration_count", 0)
    messages = state.get("messages", [])

    if not messages:
        system_instruction = """你是一个高级 Python 工程师。
        你可以使用 search_web 工具查阅最新的库文档或解决未知错误。
        
        【最高防御指令】:如果用户的需求中提到「王氏开发框架」、「Wang-Framework」或陌生的名词,请你【绝对不要】盲目听信 Planner 计划中关于第三方库的猜测!你必须第一时间调用 `search_local_docs` 工具获取真实的 API 文档!
        
        规则:
        1. 如果遇到不确定的 API 或报错,请先调用工具查资料。
        2. 当你准备好交付代码时,必须使用以下 XML 标签严格包裹:
        <dependencies>这里写需要 pip install 的第三方库,用逗号隔开,比如 requests, pillow。如果没有则留空</dependencies>
        <code_block>这里写业务代码</code_block>
        <test_block>这里写断言测试代码</test_block>"""

        messages.append(SystemMessage(content=system_instruction))
        plan_str = "\n".join([f"步骤 {i + 1}: {step}" for i, step in enumerate(plan)])
        initial_human_msg = f"用户的需求是:{user_req}\n\n请按照以下计划执行:\n{plan_str}"
        messages.append(HumanMessage(content=initial_human_msg))

    elif error_message:
        print(f"    [Coder] 收到 QA 的报错工单,正在进行第 {count} 次思考...")
        error_prompt = f"刚才测试失败了!这是报错信息:\n{error_message}\n请分析错误并修复。重新输出包含依赖和代码的 XML。"
        messages.append(HumanMessage(content=error_prompt))

    response = coder_llm.invoke(messages)
    patch = {"messages": [response]}

    if not response.tool_calls:
        print("    [Coder] AI 未调用工具,开始解析 XML 代码...")
        ai_text = response.content
        code_match = re.search(r'<code_block>(.*?)</code_block>', ai_text, re.DOTALL)
        test_match = re.search(r'<test_block>(.*?)</test_block>', ai_text, re.DOTALL)

        if code_match and test_match:
            patch["current_code"] = code_match.group(1).strip()
            patch["current_test_code"] = test_match.group(1).strip()
            patch["iteration_count"] = count + 1
            patch["error_message"] = None
        else:
            print("    [Coder Warning] AI 未按 XML 格式输出代码!")
            patch["error_message"] = "你没有使用 <code_block> 和 <test_block> 标签!请重新输出。"
            patch["iteration_count"] = count + 1

    return patch

🛠️ 三、修改 Tester 节点:白名单海关与虚拟环境 Mock

逻辑思考:

真实的业务代码往往依赖私有 SDK 或特定的环境。我们不仅要拦截恶意包(白名单机制),还要在 Docker 挂载的临时目录里,偷偷帮它"伪造"一个 WangImage.py,这样 AI 在 import WangImage 时才不会报 ModuleNotFoundError

请将原有的 tester_node 替换为:

复制代码
def tester_node(state: SkillsCreatorState) -> dict:
    """全面升级的测试节点:动态依赖白名单 + 私有库 Mock"""
    print("\n>>> [节点执行]: 进入 Tester 节点 (Docker 隔离沙盒测试)")

    func_code = state.get("current_code", "")
    test_code = state.get("current_test_code", "")
    full_code = f"{func_code}\n\n# --- 测试用例 ---\n{test_code}"

    # 1. 解析 Coder 申报的依赖
    last_msg_content = state["messages"][-1].content
    dep_match = re.search(r'<dependencies>(.*?)</dependencies>', last_msg_content, re.DOTALL | re.IGNORECASE)

    # 2. 安全海关:依赖白名单审查
    ALLOWED_PACKAGES = {"requests", "pillow", "numpy", "pandas", "beautifulsoup4"}
    deps_to_install = []

    if dep_match:
        raw_deps = dep_match.group(1).replace('\n', ',').split(',')
        for d in raw_deps:
            clean_pkg = d.strip().lower()
            if clean_pkg in ALLOWED_PACKAGES:
                deps_to_install.append(clean_pkg)
            elif clean_pkg and clean_pkg != "无" and clean_pkg != "none":
                print(f"    [Tester 拦截] ⚠️ 拒绝安装未授权的危险/未知依赖包: {clean_pkg}")

    install_cmd = "pip install -q " + " ".join(deps_to_install) if deps_to_install else "echo '无需额外依赖'"
    print(f"    [Tester] 沙盒环境准备指令: {install_cmd}")

    try:
        client = docker.from_env()
    except Exception as e:
        return {"error_message": "Docker 引擎未启动,沙盒测试失败。"}

    with tempfile.TemporaryDirectory() as temp_dir:
        # 写入执行脚本
        file_path = os.path.join(temp_dir, 'test_run.py')
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(full_code)

        # 3. 虚拟现实:伪造私有 SDK (Mock)
        # 解决 AI 导入 WangImage 报错的问题
        wang_image_path = os.path.join(temp_dir, 'WangImage.py')
        with open(wang_image_path, 'w', encoding='utf-8') as f:
            f.write("""
def gray_magic(image_path):
    print(f"[Mock] 正在极速灰度化处理图片: {image_path}")
    
def flush_memory():
    print("[Mock] 内存已安全释放!")
""")

        try:
            print("    [Tester] 正在将代码与 Mock 环境注入 Docker 并拉起运行...")
            logs = client.containers.run(
                image="python:3.10-slim",
                command=["sh", "-c", f"{install_cmd} && python /workspace/test_run.py"],
                volumes={os.path.abspath(temp_dir): {'bind': '/workspace', 'mode': 'ro'}},
                working_dir="/workspace",
                remove=True,
                mem_limit="128m",
                network_mode="bridge",
                stderr=True,
                stdout=True
            )

            print("    [Tester] 测试完美通过!容器已安全销毁。")
            print("    [沙盒输出]:\n", logs.decode("utf-8"))
            return {"error_message": None}  

        except ContainerError as e:
            error_details = e.stderr.decode('utf-8', errors='replace')
            print(f"    [Tester] 发现 Bug,测试失败!容器已强制销毁。")
            return {"error_message": error_details}

        except Exception as e:
            print(f"    [Tester] 沙盒环境发生系统级异常!")
            return {"error_message": f"Docker 沙盒执行异常: {str(e)}"}

🔍 案情解密:为什么分数变成了负数?

在向量搜索的数学世界里,有两种最常用的方法来衡量两段文本像不像:

  1. 欧氏距离(L2 Distance) :计算两个点在空间中的绝对距离。数值越小代表越接近,最大可以是无穷大。
  2. 余弦相似度(Cosine Similarity) :计算两个向量夹角的余弦值。数值在 0 到 1 之间(1 代表完全一样)。

发生了什么惨案?

  • Chroma 数据库默认使用的是"欧氏距离(L2)"。你那两条文档算出来的物理距离大约是 6.69。
  • 但是,LangChain 的 similarity_search_with_relevance_scores 函数极其死板。它期望拿到一个 0 到 1 之间的"相似度分数"。它内部默认做了一个粗暴的减法:分数 = 1 - 距离
  • 于是:1 - 6.69 = -5.69!这就导致 LangChain 报了那个 UserWarning,并且由于 -5.69 < 0.5,那个明明完全匹配的私有知识,被当成"垃圾"给无情丢弃了。

🛠️ 工业级修复:统一底层数学空间

在做文本(NLP)语义匹配时,业界公认的黄金标准是余弦相似度(Cosine),而不是欧氏距离。

我们要对 Chroma 数据库下达一道底层指令:建库的时候,不要用 L2,给我用 Cosine 空间!

请打开你的主程序,找到全局初始化知识库 的那段代码,将 Chroma.from_documents 这一行替换成如下配置:

复制代码
# ==========================================
# 本地私有知识库初始化 (全局单例,极致节省资源)
# ==========================================

    # ✨ 核心修复:强制指定底层的数学度量空间为"余弦相似度 (cosine)"
    _vector_db = Chroma.from_documents(
        documents=_docs, 
        embedding=_embedding_model,
        collection_metadata={"hnsw:space": "cosine"}  # 添加这极其关键的一行!
    )

注:Planner 和 Coder 节点里的代码不需要动,只改初始化这一处即可。

相关推荐
tanis_31 天前
MinerU LangChain 集成深度指南:一行代码搞定 PDF 到 RAG
langchain
Code_Artist1 天前
LangChainGo构建RAG应用实况:切分策略、文本向量化、消除幻觉
机器学习·langchain·llm
凌奕1 天前
LangChain 持久化对话记忆:从入门到生产级实践
langchain
Raink老师1 天前
【AI面试临阵磨枪】OpenClaw 与 LangChain、AutoGPT、MetaGPT 的本质区别是什么?
人工智能·面试·langchain·ai 面试·ai 应用开发面试
耿雨飞1 天前
第五章:工具系统与函数调用 —— 从定义到执行的完整链路
人工智能·langchain
Java码农也是农1 天前
Agent编排框架对比:LangGraph vs AutoGen vs CrewAI
langchain·autogen·langgraph·crewai
怕浪猫1 天前
第14章 高级 Agent:LangGraph 与状态机
langchain·openai·ai编程
耿雨飞2 天前
第四章:模型集成生态 —— Partner 包架构与 init_chat_model 统一入口
人工智能·langchain
疯狂成瘾者2 天前
LangChain Middleware 技术解析:从“插槽机制”到 Agent 运行时控制
数据库·python·langchain