将AgentScope的RAG能力集成到Strands Agent的实践

参考资料

在构建智能对话系统时,我们常常面临一个核心问题:如何让 AI 助手能够基于特定领域的知识或私有数据进行准确回答?传统的做法有两种:要么微调大模型,要么在提示词中直接提供上下文。但这两种方法都有明显的局限性。微调成本高昂且需要持续更新,而提示词有长度限制,且无法处理大规模知识库。

RAG(Retrieval-Augmented Generation,检索增强生成)技术解决这个问题的核心思想是:在回答用户问题时,先从一个知识库中检索相关文档,然后将检索到的内容作为上下文提供给大模型,最后生成回答。

在实际工作场景中我需要使用 Strands Agent SDK 作为主要的开发框架。然而Strands SDK 本身虽然具备了RAG tools,但是Strands 本身似乎更倾向于和Bedrock等官方服务集成。如果从零开始实现 RAG的集成,则需要用户处理本分块、向量嵌入生成、向量数据库存储、相似度检索等一系列复杂问题。与此同时,AgentScope 作为一个开源成熟且活跃的多智能体框架,已经内置了完整的 RAG 实现。它提供了开箱即用的文本读取器、多种向量存储后端、以及简单的知识库管理接口(如果可能的话直接拥抱AgentScope)。

那么如果将 AgentScope 的 RAG 能力集成到 Strands Agent 中,就能够复用 AgentScope 的 RAG 组件和实现了。这个过程有几个关键挑战:

  • 架构差异。AgentScope 的 RAG 组件是基于异步设计的,而 Strands 的工具调用机制默认是同步的。需要协调这两种不同的编程范式

  • 工具封装。Strands 使用装饰器(@tool)将函数转换为可被 Agent 调用的工具,需要确保 AgentScope 的 RAG 功能能够以这种形式被暴露。

  • 状态管理。知识库是有状态的,需要在多次对话中持久化。如何管理这些知识库实例?

  • 错误处理。RAG 涉及网络调用、向量计算等复杂操作,如何优雅地处理各种可能的错误?

带着这些挑战和思考,我们开始了集成实践。

AgentScope的RAG架构

AgentScope 的RAG 实现遵循一个清晰的架构模式,由三个核心组件构成:

  • Reader:负责从数据源读取数据并进行分块。将长文本切分成合适的块是后续有效检索的基础。AgentScope 提供了多种 Reader,比如 TextReader 用于纯文本,PDFReader 用于 PDF 文档,甚至支持 ImageReader 处理图像。Reader 的输出是统一的 Document 对象,包含了原始内容、元数据等信息。
  • Knowledge:负责知识库查询检索和数据存储的逻辑。这是 RAG 的核心组件,封装了如何使用嵌入模型和向量存储。
  • Store:负责与向量数据库的交互。这是 RAG 的底层存储层,实际保存和检索向量数据。AgentScope 目前内置了对 Qdrant 的支持。Qdrant 是一个性能优异的开源向量数据库,提供了多种部署方式:内存模式、本地持久化模式、云端托管模式。对于测试和开发可以选择内存模式,不需要额外的配置,数据保存在内存中,程序退出后就消失。但是在生产环境中,应该使用本地持久化或云端模式。

通过 AgentScope 简洁的设计,创建一个完整的 RAG 知识库只需要几行代码。关于SimpleKnowledge的示例代码参考,通过继承KnowledgeBase并实现retrieve和add_documents方法

py 复制代码
from agentscope.rag import SimpleKnowledge
from agentscope.embedding import OpenAITextEmbedding
from agentscope.rag import QdrantStore
# 创建嵌入模型
emb_model = OpenAITextEmbedding(
    model_name="text-embedding-3-small",
    api_key="your-api-key",
    dimensions=1536,
    base_url="http://localhost:4000/v1",
)
# 创建知识库
knowledge = SimpleKnowledge(
    embedding_model=emb_model,
    embedding_store=QdrantStore(
        location=":memory:",  # 内存模式
        collection_name="my_kb",
        dimensions=1536,
    ),
)

在文档加载方面,TextReader 提供了两个关键参数:chunk_size 控制每个文本块的大小,split_by 决定按什么方式切分(比如按段落、按句子等)。因为文本块的大小直接影响 RAG 的效果。块太大,检索时可能包含太多无关信息,导致回答不够聚焦;块太小,又可能丢失重要的上下文,导致回答不完整。通常,对于中文文档,512 到 1024 个字符的块大小是比较合适的起点。

切分方式也很关键。按段落切分可以保持语义的完整性,按句子切分则可以提供更精细的检索粒度。根据具体的应用场景,可以选择不同的策略。

Strands的工具调用机制

在 Strands 中,将一个函数转换为 Agent 可调用工具只需使用 @tool 装饰器。

当你用 @tool 装饰一个函数时,Strands 会自动提取函数的元数据:包括函数名、参数类型、返回类型、以及docstring。这些元数据被转换成 OpenAI 兼容的工具规范,这使得大模型能够理解工具的能力和使用方法。装饰后的函数既是普通的 Python 函数,又是一个 Strands 工具。用户可以直接调用它,也可以将它传递给 Agent,让 Agent 在适当的时候自动调用它。

Strands 对工具的返回值有明确的规范:必须是一个字典,包含 "status" 字段(表示成功或失败)和 "content" 字段(包含实际的内容)。content 通常是一个列表,每个元素是一个字典,包含 "text" 字段。这种规范确保了工具调用的结果能够被正确地格式化和展示给用户。同时,它也让错误处理变得统一:工具只需要捕获异常并返回错误状态,上层逻辑可以统一处理。

将AgentScope RAG注册为Stands的Tool

基于对 AgentScope RAG 和 Strands 工具机制的理解,我们可以通过如下方式实现将AgentScope RAG注册为Stands的工具,并由agent自主决定RAG动作,效果如下

知识库管理

作为示例,使用 AgentScope 的 SimpleKnowledge 作为知识库核心。它内部管理着嵌入模型和向量存储。知识库的创建过程包括:实例化嵌入模型、实例化向量存储、将两者组合成 SimpleKnowledge。

py 复制代码
def create_knowledge_base(
    name: str = "default",
    embedding_model: str = "text-embedding-3-small",
    dimensions: int = 1536,  # text-embedding-3-small 的维度
    api_key: Optional[str] = None,
    base_url: Optional[str] = None,
) -> SimpleKnowledge:
    # 创建嵌入模型
    emb_model = OpenAITextEmbedding(
        model_name=embedding_model,
        api_key=api_key or os.getenv("OPENAI_API_KEY", "sk-D6TQa6a-echLxddGr52kXQ"),
        dimensions=dimensions,
        base_url=base_url or os.getenv("OPENAI_BASE_URL", "http://localhost:4000/v1"),
    )

    # 创建知识库(使用内存模式的 Qdrant)
    knowledge = SimpleKnowledge(
        embedding_model=emb_model,
        embedding_store=QdrantStore(
            location=":memory:",
            collection_name=f"kb_{name}",
            dimensions=dimensions,
        ),
    )

    return knowledge

此外,实现全局的知识库缓存机制,使用字典来存储不同名称的知识库实例,以支持多个独立的知识库。当创建知识库时,首先检查缓存中是否已存在。懒加载的模式既提高了性能,又允许按需创建知识库。

py 复制代码
def get_knowledge_base(name: str = "default", **kwargs) -> SimpleKnowledge:
    """获取或创建知识库实例"""
    if name not in _knowledge_bases:
        _knowledge_bases[name] = create_knowledge_base(name=name, **kwargs)
    return _knowledge_bases[name]

工具函数的实现

使用 Strands 的 @tool 装饰器将这些辅助函数包装成工具,使其可以直接被 Strands Agent 使用。实现了五个核心工具:

  • rag_query:用于从知识库检索相关文档。这个工具接收查询文本、知识库名称和返回数量等参数,内部调用知识库的 retrieve 方法,然后格式化结果。检索到的文档会包含相似度分数,这有助于评估检索的质量。
  • rag_add_document:用于将单个文档添加到知识库。它使用 TextReader 将文本分块,然后调用知识库的 add_documents 方法。这个工具允许用户在对话过程中动态地向知识库添加信息。
  • rag_load_file:用于从文件批量加载知识。它支持 .txt 和 .md 文件,读取文件内容后按段落切分,然后批量添加。这对于导入已有的文档库特别有用。
  • rag_stats:用于查看知识库的统计信息。它返回知识库名称、文档数量、存储类型等元数据,帮助用户了解知识库的状态。
  • rag_clear:用于清空知识库。这在重新开始或清理测试数据时很有用。实现方式是从缓存中删除知识库实例,下次使用时会重新创建一个空的。

此外,AgentScope 的 RAG 操作(添加文档、检索)都是异步的,因为涉及网络 I/O 和向量计算。但 Strands 的工具函数是同步的(似乎可以通过Hook来实现回调,但是不够优雅)。需要创建一组辅助函数封装与知识库交互的异步操作,并在工具函数内部使用 asyncio.run 来桥接异步和strands sdk的同步方法。

在工具函数内部使用 asyncio.run()。这个函数会启动一个事件循环,运行异步函数,然后阻塞等待结果,最后将结果返回。这样就实现了从异步到同步的转换。

例如rag_add_document方法的实现逻辑如下

py 复制代码
from strands import tool
...
@tool
def rag_add_document(
    content: str,
    knowledge_base: str = "default",
) -> dict:
    """
    添加新文档到知识库

    使用此工具将重要信息添加到知识库,以便在后续对话中能够检索和使用这些信息。
    适合添加用户提供的知识点、事实陈述或需要记住的内容。

    参数:
        content: 要添加到知识库的文档内容(文本)
        knowledge_base: 要添加到的知识库名称(默认为 "default")

    返回:
        包含操作结果的字典
    """
    try:
        count = asyncio.run(add_text_to_kb(content, knowledge_base))

        return {
            "status": "success",
            "content": [
                {"text": f"✓ 已添加 {count} 个文档块到知识库 '{knowledge_base}'"}
            ],
        }
    except Exception as e:
        return {
            "status": "error",
            "content": [{"text": f"添加文档时出错: {str(e)}"}],
        }

文档切分的策略

在实现中,我选择了按段落切分(split_by="paragraph"),块大小设为 512。这是一个经验值,但对于不同的应用场景可能需要调整。

  • 如果文档主要是连续的长文本,按段落切分能保持语义完整性。如果文档是结构化的(比如有明确的章节划分),可能需要考虑更智能的切分方式。

  • 块大小的选择也需要权衡。太小会导致上下文破碎,太大又会导致检索不够精确。在实践中,可能需要通过实验来找到最优值。

py 复制代码
async def add_text_to_kb(
    text: str,
    kb_name: str = "default",
    chunk_size: int = 512,
) -> int:
    kb = get_knowledge_base(kb_name)

    # 使用 TextReader 分块文本
    reader = TextReader(chunk_size=chunk_size, split_by="paragraph")
    documents = await reader(text=text)

    # 添加到知识库
    await kb.add_documents(documents)

    return len(documents)

此外,AgentScope 允许自定义 Reader。如果内置的 TextReader 不能满足需求,可以继承 BaseReader 类并实现自己的切分逻辑。

为Strands实现通用的RAG方式

agentscope的ReActAgent 还提供了通用的rag方法, 在每次 reply 函数开始执行时检索知识,并将检索到的知识附加到用户消息的提示中。

具体而言每次 reply() 方法开始时都会执行 _retrieve_from_knowledge方法,并通过kb.retrieve(query=query)检索上下文

py 复制代码
async def reply(self, msg: Msg | list[Msg] | None = None, ...) -> Msg:
    # 1. 记录用户消息
    await self.memory.add(msg)
    
    # 2. 自动检索知识
    # -------------- Retrieval process --------------
    # Retrieve relevant records from the long-term memory if activated
    await self._retrieve_from_long_term_memory(msg)
    # Retrieve relevant documents from the knowledge base(s) if any
    await self._retrieve_from_knowledge(msg)
    
    # 3. 进入推理-行动循环
    for _ in range(self.max_iters):
        msg_reasoning = await self._reasoning(tool_choice)
        ...

Strands Hook机制

那么如何在Strands中实现类似通用RAG逻辑?可以考虑使用Hook功能

Strands中的Hook回调函数针对特定的事件类型进行注册,并在代理执行过程中这些事件发生时接收强类型的事件对象。每个事件都包含与代理生命周期该阶段相关的数据。例如,BeforeInvocationEvent 包含代理和请求的详细信息。可以使用 agent.hooks 在事后为特定事件注册回调函数:

py 复制代码
agent = Agent()

# Register individual callbacks
def my_callback(event: BeforeInvocationEvent) -> None:
    print("Custom callback triggered")

agent.hooks.add_callback(BeforeInvocationEvent, my_callback)

具体实现rag的hook示例如下

py 复制代码
from strands.hooks.events import BeforeModelCallEvent
class RAGAutoInjector(HookProvider):
    def __init__(self, knowledge, top_k: int = 3, enable_logging: bool = True):
        self.knowledge = knowledge
        self.top_k = top_k
        self.enable_logging = enable_logging

    def register_hooks(self, registry, **kwargs):
        registry.add_callback(BeforeModelCallEvent, self.on_before_model_call)

    def on_before_model_call(self, event):
        messages = event.agent.messages
        if not messages:
            return
        last_msg = messages[-1]
        query = self._extract_query(last_msg)
        if not query:
            return
        try:
            import nest_asyncio # 允许在已有的事件循环中嵌套运行异步代码,解决 "asyncio.run() cannot be called from a running event loop" 错误
            nest_asyncio.apply()
            async def retrieve():
                docs = await self.knowledge.retrieve(query=query, limit=self.top_k)
                docs.sort(key=lambda doc: doc.score or 0.0, reverse=True)
                return docs[: self.top_k]
            loop = asyncio.get_event_loop()
            docs = loop.run_until_complete(retrieve())
            if not docs:
                return

            rag_context = "\n\n".join([doc.metadata["content"]["text"] for doc in docs])
            enhanced_query = (
                f"{query}\n\n"
                f"<retrieved_knowledge>\n"
                f"以下是从知识库检索到的相关信息:\n\n"
                f"{rag_context}\n"
                f"</retrieved_knowledge>"
            )
            
            # 修改消息 - 保持原始格式
            if isinstance(last_msg, str):
                event.agent.messages[-1] = enhanced_query
            elif isinstance(last_msg, dict):
                content = last_msg.get("content")
                if isinstance(content, str):
                    event.agent.messages[-1]["content"] = enhanced_query
                elif isinstance(content, list) and content:
                    # Strands 格式: {'role': 'user', 'content': [{'text': 'query'}]}
                    event.agent.messages[-1]["content"][0]["text"] = enhanced_query
        except Exception as e:
            if self.enable_logging:
                print(f"[Hook] ✗ 检索失败: {e}\n")

在agent中注册Hook

py 复制代码
from strands.models.openai import OpenAIModel
knowledge = get_knowledge_base("default")
hook = RAGAutoInjector(knowledge=kb, top_k=2)
# 创建 Agent,使用 Hook 自动注入
agent = Agent(
    model=OpenAIModel(...),
    hooks=[hook],
    system_prompt="""你是一个智能助手。基于提供的检索知识回答用户问题。如果检索结果中没有相关信息,基于你的训练知识回答""",
)

效果如下

相关推荐
EchoMind-Henry3 小时前
EchoMindBot_v1.0.0 发布了
人工智能·ai·ai agent 研发手记
躺柒6 小时前
读人工智能全球格局:未来趋势与中国位势06人类的未来(下)
大数据·人工智能·算法·ai·智能
XLYcmy8 小时前
智能体大赛 技术架构 数据根基层
数据库·ai·llm·api·agent·幻觉·万方
㱘郳9 小时前
AI模型输出内容转飞书Markdown
ai·飞书
PPIO派欧云9 小时前
Qwen3.5重磅发布 PPIO 模型服务平台同步上线
ai·大模型
x-cmd10 小时前
Browser-Use:用自然语言控制浏览器,告别脆弱的自动化脚本
运维·ai·自动化·agent·浏览器·x-cmd
XLYcmy10 小时前
智能体大赛 核心功能 可信文献检索与系统性知识梳理
数据库·ai·llm·prompt·知识图谱·agent·检索
阿杰学AI13 小时前
AI核心知识108—大语言模型之 AI Aesthetics Engineer(简洁且通俗易懂版)
人工智能·ai·语言模型·自然语言处理·aigc·新型职业·ai美学工程师
逻极13 小时前
BMAD之核心架构:为什么“方案化”至关重要 (Phase 3 Solutioning)——必学!BMAD 方法论架构从入门到精通
人工智能·ai·系统架构·ai编程·敏捷开发·ai辅助编程·bmad