Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

内存知识智能体 (Agent RAG)

本 Notebook 演示如何读取 files 目录下的 .txt 文件,按空行切分段落,并构建向量检索问答助手。

前置要求

  1. 确保已安装必要依赖 (langchain, openai 等)。

  2. 确保当前 Notebook 同级目录下存在 files 文件夹,且其中包含你的 .txt 文件。

1. 配置环境与模型

import os
from dotenv import load_dotenv
from openai import OpenAI
from langchain_openai import ChatOpenAI

# 加载环境变量
load_dotenv()

# 配置 API Key (如未在 .env 设置,请在此处修改)
if not os.getenv("DASHSCOPE_API_KEY"):
    # os.environ["DASHSCOPE_API_KEY"] = "sk-xxxxxxxx"
    print("⚠️ 请设置 DASHSCOPE_API_KEY")

os.environ["DASHSCOPE_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1"

# 初始化 LLM
llm = ChatOpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
    model="qwen3-coder-plus",
    temperature=0,
)

# 初始化 OpenAI 客户端 (用于 Embedding)
client = OpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
)

2. 定义 Embedding 类

from langchain_core.embeddings import Embeddings

class DashScopeEmbeddings(Embeddings):
    """DashScope 兼容的 Embeddings 封装。"""

    def __init__(self, model: str = "text-embedding-v4", dimensions: int = 1024):
        self.model = model
        self.dimensions = dimensions

    def embed_documents(self, texts: list[str]) -> list[list[float]]:
        vectors = []
        # 简单批处理,每次 10 条
        for i in range(0, len(texts), 10):
            chunk = texts[i : i + 10]
            try:
                response = client.embeddings.create(
                    model=self.model,
                    input=chunk,
                    dimensions=self.dimensions,
                )
                vectors.extend([item.embedding for item in response.data])
            except Exception as e:
                print(f"Embedding Error: {e}")
                vectors.extend([[0.0] * self.dimensions] * len(chunk))
        return vectors

    def embed_query(self, text: str) -> list[float]:
        response = client.embeddings.create(
            model=self.model,
            input=[text],
            dimensions=self.dimensions,
        )
        return response.data[0].embedding

3. 读取现有文档并切分

此处将读取 files/ 目录下的所有 .txt 文件,并使用正则表达式按空行进行切分(也可以按嵌入模型的维度切分)。

import re
from pathlib import Path
from typing import Iterable
from langchain_core.documents import Document

def load_txt_documents(target_dir: Path) -> list[Document]:
    """读取目录下的 txt 文件并按空行分割为 Document。"""

    def split_on_blank(text: str) -> Iterable[str]:
        # 按dimensions维度分割(一版用于通识文档)
        # for block in re.split(rf"\n\s*{{{self.dimensions}}}\s*\n", text):
        
        # 按空行分割(兼容 Windows \r\n)
        for block in re.split(r"\n\s*\n", text):
            cleaned = block.strip()
            if cleaned:
                yield cleaned

    documents: list[Document] = []
    
    # 检查目录
    if not target_dir.exists():
        print(f"❌ 错误: 目录 {target_dir} 不存在!请确认当前路径。")
        return []

    files = sorted(target_dir.glob("*.txt"))
    if not files:
        print(f"❌ 警告: {target_dir} 下没有找到 .txt 文件")
        return []

    for path in files:
        print(f"📄 正在读取: {path.name}")
        content = path.read_text(encoding="utf-8")
        for idx, part in enumerate(split_on_blank(content)):
            documents.append(
                Document(
                    page_content=part,
                    metadata={"source": path.name, "chunk_id": idx},
                )
            )
    return documents

4. 构建向量库

from langchain_core.vectorstores import InMemoryVectorStore

def build_vector_store() -> InMemoryVectorStore:
    # 指定 files 目录 (相对于当前 Notebook)
    target_dir = Path.cwd() / "files"
    documents = load_txt_documents(target_dir)

    embeddings = DashScopeEmbeddings()
    vector_store = InMemoryVectorStore(embedding=embeddings)
    
    if documents:
        vector_store.add_documents(documents)
        print(f"\n✅ 嵌入完成: 共 {len(documents)} 个片段。")
    
    return vector_store

# 立即构建
vector_store = build_vector_store()
📄 正在读取: question.txt

✅ 嵌入完成: 共 8 个片段。

5. 创建检索 Agent

from langchain.agents import create_agent
from langchain.tools import tool

def create_react_agent(vector_store: InMemoryVectorStore):
    """创建带检索工具的 ReAct Agent。"""

    @tool(response_format="content_and_artifact")
    def retrieve_context(query: str):
        """基于向量库检索与问题最相关的文本片段。"""
        # 打印日志方便调试
        print(f"   🔎 [检索中] Query: {query}")
        
        retrieved = vector_store.similarity_search(query, k=3)
        
        serialized = "\n\n".join(
            f"[{doc.metadata['source']}#{doc.metadata['chunk_id']}] {doc.page_content}"
            for doc in retrieved
        )
        return serialized, retrieved

    return create_agent(
        llm,
        tools=[retrieve_context],
        system_prompt=(
            "你可以使用检索工具获得参考资料。回答时结合检索到的内容,"
            "如有必要可以在答案中简单引用来源标识。"
        ),
    )

6. 提问测试

请根据你的 files 目录中的文档内容修改下面的 query 变量。

# 初始化 Agent
agent = create_react_agent(vector_store)

# === 修改此处的问题 ===
query = "考勤缺卡怎么处理?"

print(f"🚀 用户问题: {query}\n")

# 流式输出回答
for event in agent.stream({"messages": [{"role": "user", "content": query}]}, stream_mode="values"):
    event["messages"][-1].pretty_print()
🚀 用户问题: 考勤缺卡怎么处理?

================================ Human Message =================================

考勤缺卡怎么处理?
================================== Ai Message ==================================
Tool Calls:
  retrieve_context (call_f714d4e689774d979ecafd22)
 Call ID: call_f714d4e689774d979ecafd22
  Args:
    query: 考勤缺卡怎么处理?
   🔎 [检索中] Query: 考勤缺卡怎么处理?
================================= Tool Message =================================
Name: retrieve_context

[question.txt#3] 问题:忘记打卡怎么办?
答案:员工缺卡可在缺卡发生后的3个工作日内申请补卡,每人每月限3次,超限不再受理。未补卡者,每缺1次按缺勤半天计算,扣发半天工资。
权限:
关键词:因公外出、打卡处理

[question.txt#4] 问题:因公外出无法回公司打卡怎么办?
答案:允许打外勤卡,需走外出申请并注明原因;外出申请路径:飞书/更多/工作台/人力资源&行政/假勤/申请/外出。
权限:
关键词:因公外出,无法打卡

[question.txt#2] 问题:考勤有问题找谁?
答案:找LarryLi。
权限:
关键词:考勤问题
================================== Ai Message ==================================

如果你遇到了考勤缺卡的问题,可以根据以下情况处理:

1. **忘记打卡**:根据规定,员工缺卡可在缺卡发生后的3个工作日内申请补卡,每人每月限补卡3次,超过限制将不再受理。如果未能及时补卡,每缺卡一次将按缺勤半天计算,并扣发半天工资。[[3]]

2. **因公外出无法回公司打卡**:如果是因公外出导致无法打卡,可以申请打外勤卡。你需要提前走外出申请流程并注明原因。具体操作路径为:飞书 → 更多 → 工作台 → 人力资源&行政 → 假勤 → 申请 → 外出。[[4]]

如果有其他考勤相关问题,可以联系 LarryLi 进行咨询。[[2]]