Arcana:Elixir原生嵌入式RAG库,一体化智能检索与生成方案
1. 项目概述:一个为Elixir生态量身打造的嵌入式RAG库
如果你正在用Elixir和Phoenix构建应用,并且想为它加上一个智能的“知识大脑”,让应用能理解、检索并回答用户基于你私有数据的问题,那么Arcana就是你一直在找的那个工具。它不是一个需要你额外维护一个Python服务、一个独立向量数据库的“外挂”系统,而是一个能直接嵌入到你现有Phoenix应用生命周期的原生库。简单来说,Arcana让你能用Elixir的方式,在BEAM虚拟机上,构建从文档处理、向量化、知识图谱到智能检索与生成(RAG)的完整能力,所有数据都存放在你已有的PostgreSQL数据库中。
为什么这很重要?传统的RAG方案往往意味着技术栈的割裂:用Python写数据处理和模型服务,用HTTP与你的主应用通信,再单独维护一个向量数据库(如Pinecone、Weaviate)。这不仅增加了运维复杂度,也让调试、监控和事务一致性变得棘手。Arcana的核心哲学是“一体化”:你的Ecto仓库(Repo)就是向量存储,你的Phoenix监督树(Supervision Tree)承载着嵌入模型和重排序器,你的LiveView可以直接变身管理面板,而应用内建的Telemetry则提供了完整的可观测性。它充分利用了Elixir/OTP在并发、容错和热升级方面的天然优势,将RAG从一个外部服务变成了应用的一个有机组成部分。
2. 核心设计理念:为何选择“嵌入式”与“一体化”
2.1 技术栈统一的优势
在深入代码之前,理解Arcana的设计选择至关重要。大多数RAG库诞生于Python生态,这本身无可厚非,因为AI模型生态的重心在那里。但当你要将其集成到一个Elixir/Phoenix应用中时,就会面临“胶水代码”的困境:数据需要在两个系统间同步,错误处理需要跨进程协调,链路追踪变得支离破碎。Arcana的创始人George Guimarães敏锐地意识到,BEAM虚拟机(Erlang运行时)的特性——轻量级进程、监督树、内置的:telemetry——本身就是构建稳健、可观测的RAG系统的绝佳基础。
一个Repo统治所有:Arcana将文档、文本块(Chunk)、向量嵌入、实体关系等所有数据,都通过迁移文件创建为标准的Ecto Schema,存储在同一个PostgreSQL数据库中,并利用pgvector扩展来处理向量相似度搜索。这意味着:
- 数据一致性:文档的增删改查可以与你的用户、订单等业务数据在同一个数据库事务中完成。
- 简化运维:无需部署、监控另一个数据库服务。备份、恢复、连接池管理都是你熟悉的那一套。
- 利用现有技能:你的团队不需要学习新的查询语言或管理工具,用Ecto和SQL就能操作所有RAG相关数据。
本地优先的计算:Arcana默认使用Bumblebee(Elixir的神经网络库)在本地运行嵌入模型和交叉编码器(Cross-Encoder)重排序模型。它支持Nx(Elixir的数值计算库)的后端,如EXLA(用于GPU/TPU加速)、EMLX(针对Apple Silicon优化)和Torchx。这带来了显著的延迟降低和成本节省,尤其对于处理敏感数据或需要高频调用的场景。当然,它也完全支持切换到OpenAI、Cohere等云端API,但这只是一个可选项,而非必选项。
2.2 面向不同场景的三种使用模式
Arcana没有提供“一刀切”的接口,而是根据任务的复杂度和对控制流的需求,提供了三个层次的使用界面,这直接对应了学术界对Agentic RAG的分类。
2.2.1 高级RAG模式:Arcana.search/2与Arcana.ask/2这是最常用的入口,对应“Advanced RAG”。你只需要一个函数调用,它内部就自动完成了查询重写、混合搜索(向量+全文)、可选的图谱融合以及交叉编码器重排序。适合绝大多数问答和检索场景。例如,用户问“Phoenix怎么处理实时更新?”,Arcana.ask/2会先将其重写为更规范的查询语句,然后从你的知识库中检索相关片段,最后让LLM生成一个连贯的答案。
2.2.2 模块化RAG模式:Arcana.Pipeline当你需要对检索-生成流程的每一步进行精细控制时,就该使用Arcana.Pipeline了,它对应“Modular RAG”。你可以像组装乐高一样,自定义流程的步骤和顺序。Pipeline暴露了从gate(判断是否需要检索)、rewrite(查询重写)、decompose(问题分解)、search、rerank到answer(生成)和ground(事实性核查)的每一个环节。每个环节都是一个行为(Behaviour),你可以用短短十几行代码实现自己的逻辑来替换默认实现。这为处理复杂查询(如需要多步推理的比较性问题)提供了极大的灵活性。
2.2.3 智能体RAG模式:Arcana.Loop这是最前沿的模式,对应“Agentic RAG”。在这种模式下,控制权被交给了LLM本身。你向Arcana.Loop提出一个开放性问题(例如,“找出所有博士被同伴背叛的剧集”),然后启动循环。在每一轮迭代中,一个作为“控制器”的LLM(通常选用较小、较快的模型)会评估当前上下文,并自主决定调用哪个工具:search(继续搜索)、answer(给出最终答案)或give_up(放弃)。它可能会进行多轮搜索,逐步收集信息,直到认为自己可以回答或达到迭代上限。这种模式特别适合答案隐藏在多次“跳跃”(Multi-hop)检索中的复杂问题。
3. 实战入门:从零开始集成Arcana
3.1 环境准备与安装
假设你已经有一个正在运行的Phoenix 1.7+应用,并且使用PostgreSQL作为数据库。集成Arcana最快捷的方式是使用Igniter(一个Elixir项目模板工具)。
# 在你的Phoenix项目根目录下执行 mix igniter.install arcana这个命令会完成以下几件事:
- 在
mix.exs中添加Arcana依赖。 - 生成数据库迁移文件,用于创建
documents、chunks、embeddings、entities等表。 - 在
config/config.exs中为你的Repo添加Arcana.Repo配置。 - 在
lib/your_app/application.ex的监督树中启动必要的Arcana服务(如嵌入服务)。 - 在
router.ex中挂载Arcana的LiveView管理面板路由。
接下来,运行迁移以创建表结构:
mix ecto.migrate手动安装详解:如果你希望更细致地控制,或者项目结构特殊,也可以手动安装。核心步骤包括:
- 添加依赖:在
mix.exs的deps函数中添加{:arcana, "~> 0.1"}。 - 配置数据库:确保你的
config/dev.exs等环境配置中,为Ecto Repo启用了pgvector扩展。这通常在Repo配置的after_connect回调中完成。 - 配置嵌入服务:这是关键一步。你需要在
application.ex的start/2函数中,将嵌入服务(Arcana.Embedder)和重排序服务(Arcana.Reranker)作为子进程启动。Arcana提供了Arcana.Embedder.Bumblebee等实现,你需要指定使用的模型(如Bumblebee.Models.HfTextEmbedding.all_minilm_l6_v2())。 - 运行迁移:手动复制Arcana提供的迁移文件到你的
priv/repo/migrations目录并执行mix ecto.migrate。
3.2 首次数据摄入与查询
安装并启动服务后,你就可以开始向知识库添加内容了。数据摄入(Ingestion)是构建RAG系统的第一步。
3.2.1 摄入纯文本最简单的方式是直接摄入一段文本。Arcana会自动将其分块、生成向量并存储。
# 在IEx中或你的业务逻辑中 alias MyApp.Repo {:ok, document} = Arcana.ingest( """ Phoenix LiveView 是一个用于构建实时、服务器渲染用户界面的Elixir库。 它消除了对客户端JavaScript框架的依赖,通过持久的WebSocket连接在服务器和客户端之间同步状态。 LiveView的核心是“状态在服务端,DOM差异通过WebSocket推送”的模型。 """, repo: Repo, collection: "elixir-guides" # 可选:将文档归类到某个集合中 )ingest/2函数会返回一个{:ok, %Arcana.Document{}}元组。在后台,它完成了:
- 分块:使用默认或配置的分块器(Chunker)将长文本切割成有重叠的小段,以优化检索效果。
- 嵌入:调用你配置的嵌入服务,为每个文本块生成高维向量。
- 存储:将文档元数据、文本块和对应的向量存入数据库。
3.2.2 进行首次智能问答数据摄入后,立刻就可以进行查询。
# 使用最简单的 ask/2 接口 {:ok, answer_ctx} = Arcana.ask("Phoenix LiveView 是什么?", repo: Repo, llm: "openai:gpt-4o-mini" # 指定使用的LLM,格式为 `提供商:模型名` ) IO.puts answer_ctx.answer # 输出可能类似于:“Phoenix LiveView 是一个允许开发者使用Elixir构建实时、交互式Web界面的库...”ask/2在内部调用了search/2进行检索,然后将检索到的相关文本块作为上下文,连同问题一起发送给LLM生成答案。你还可以通过search/2直接获取检索结果,而不生成答案,用于构建自定义的交互流程。
3.3 核心配置详解:嵌入模型与LLM提供商
嵌入模型配置:Arcana的威力很大程度上取决于嵌入模型的质量。本地运行all-MiniLM-L6-v2是一个不错的起点,它在速度和效果上取得了很好的平衡。配置在config/config.exs中:
config :arcana, :embedder, serving: [ module: Arcana.Embedder.Bumblebee, model: {"sentence-transformers/all-MiniLM-L6-v2", Bumblebee.Models.HfTextEmbedding.all_minilm_l6_v2()}, batch_size: 32, defn_options: [compiler: EXLA] # 使用EXLA编译器进行GPU加速 ]如果你的机器有NVIDIA GPU,使用EXLA后端可以极大提升向量化速度。对于Apple Silicon Mac,可以配置为EMLX。
LLM提供商集成:Arcana通过Arcana.LLM行为抽象了与不同大模型API的交互。它内置了OpenAI、Anthropic、Z.ai等提供商的适配器。配置通常在环境变量或runtime.exs中完成:
config :arcana, :llm_providers, openai: [ api_key: System.get_env("OPENAI_API_KEY"), http_options: [recv_timeout: 60_000] ], anthropic: [ api_key: System.get_env("ANTHROPIC_API_KEY") ]在调用ask/2或Loop.run/2时,通过llm: "openai:gpt-4o"这样的字符串来指定模型。你也可以实现自己的Arcana.LLM行为,接入任何兼容的API或本地模型。
注意:初次运行涉及模型下载。Bumblebee会从Hugging Face下载模型文件,这可能需要一些时间,请确保网络通畅。模型文件默认会缓存在
~/.cache/bumblebee目录下。
4. 深入核心:Pipeline模块化流程实战
当你需要超越简单的问答,处理更复杂的检索任务时,Arcana.Pipeline提供了完整的控制力。它本质上是一个可组合的数据处理管道,上下文(%Pipeline.Context{})在其中流动并被逐步丰富。
4.1 Pipeline 基础流程解析
一个典型的Pipeline调用看起来像这样:
alias Arcana.Pipeline # 1. 创建初始上下文 ctx = Pipeline.new("比较Elixir和Erlang在构建Web服务方面的优缺点", repo: MyApp.Repo, llm: "openai:gpt-4o-mini" ) # 2. 执行管道步骤 ctx = ctx |> Pipeline.rewrite() # 步骤A:重写查询,使其更规范 |> Pipeline.select(collections: ["elixir", "erlang"]) # 步骤B:让LLM选择在哪个集合中搜索 |> Pipeline.decompose() # 步骤C:将复杂问题分解成子问题 |> Pipeline.search() # 步骤D:并行搜索所有子问题 |> Pipeline.rerank() # 步骤E:对搜索结果进行重排序 |> Pipeline.answer() # 步骤F:综合所有信息生成最终答案 |> Pipeline.ground() # 步骤G:(可选)进行事实性核查 # 3. 获取结果 final_answer = ctx.answer grounding_score = ctx.grounding.score # 事实性得分,越高越好让我们拆解每个关键步骤:
rewrite/1: 用户的问题往往是口语化的、模糊的。此步骤利用LLM将问题重写为更适合检索的形式。例如,“咋用LiveView搞个聊天室?”可能被重写为“如何使用Phoenix LiveView实现一个实时聊天室应用?”
decompose/1: 对于复合型问题(如上面的比较问题),这一步会将其分解为多个独立的子问题,例如:[“Elixir构建Web服务的优势是什么?”,“Erlang构建Web服务的优势是什么?”,“两者的主要区别是什么?”]。这些子问题会被并行搜索,效率远高于用原问题一次性检索。
search/1: 这是核心检索步骤。它对每一个子问题(或原问题)执行混合搜索。默认情况下,它同时进行:
- 向量语义搜索:计算查询的向量,在
pgvector中查找余弦相似度最高的文本块。 - 全文搜索:利用PostgreSQL的
tsvector/tsquery进行关键词匹配。 - 结果融合:使用** Reciprocal Rank Fusion (RRF)** 算法将两种检索方式的结果列表融合成一个更优的最终排名。RRF能有效结合语义相关性和关键词匹配度。
rerank/1: 初步检索返回的Top-K个结果(比如20个),可能包含一些相关性稍差的片段。重排序步骤使用一个更精细的交叉编码器模型(如cross-encoder/ms-marco-MiniLM-L-6-v2),对查询和每一个候选片段进行两两交互计算,得到一个更精确的相关性分数,并据此重新排序。这通常能显著提升Top-1结果的准确率。
answer/1: 将经过重排序的最相关文本块作为上下文,连同问题一起发送给LLM,指令其生成最终答案。你可以通过max_tokens、temperature等参数控制生成过程。
ground/1: 这是一个可选的“守门员”步骤。它使用自然语言推理模型来判断生成的答案是否严格基于提供的上下文。如果答案中包含了上下文未提及的信息(即“幻觉”),则会给出一个较低的分数。这对于需要高事实准确性的场景(如客服、知识库)至关重要。
4.2 自定义Pipeline行为
Pipeline的强大之处在于它的每个步骤都是可插拔的。每个步骤都对应一个行为(Behaviour),例如Arcana.Pipeline.Step.Rewrite。默认实现已经足够好,但当你需要特殊逻辑时,可以轻松覆盖。
假设你的应用领域有特殊的术语,需要定制的查询重写规则:
defmodule MyApp.CustomRewriter do @behaviour Arcana.Pipeline.Step.Rewrite @impl true def call(%Pipeline.Context{query: query} = ctx, _opts) do # 1. 先进行一些领域特定的字符串替换 enhanced_query = String.replace(query, "咱这系统", "本订单管理系统") enhanced_query = String.replace(enhanced_query, "卡单", "订单状态停滞") # 2. 再调用默认的LLM重写逻辑(可选,也可以完全自己实现) # 这里我们复用Arcana内置的LLM重写器,但传入我们预处理后的查询 new_ctx = %Pipeline.Context{ctx | query: enhanced_query} Arcana.Pipeline.Step.Rewrite.Default.call(new_ctx, []) end end # 使用自定义的重写器 ctx |> Pipeline.rewrite(using: MyApp.CustomRewriter) |> ... # 其他步骤你可以用同样的方式自定义分块策略、搜索算法(例如集成一个外部搜索引擎)、重排序模型,甚至整个答案生成的提示词模板。这种模块化设计使得Arcana能适应从简单问答到复杂企业工作流的各种场景。
实操心得:在实现自定义步骤时,务必遵循“纯函数”理念。步骤函数应该接收一个上下文,返回一个新的上下文,避免产生副作用。这保证了Pipeline的可测试性和可组合性。另外,充分利用Arcana发出的Telemetry事件,为你自定义的步骤也添加监控,这对于后期调试和性能优化非常有帮助。
5. 高级应用:构建自主检索智能体(Agentic Loop)
Arcana.Loop代表了RAG演进的更高级形态。它不是执行一个预设的流程,而是将“下一步做什么”的决策权交给了LLM(控制器)。这适用于那些无法预先确定检索步骤的开放性问题。
5.1 Loop 的工作原理与配置
一个典型的Loop使用场景如下:
# 1. 初始化一个Loop,提出一个开放性问题 {:ok, ctx} = Arcana.Loop.new( "找出《神秘博士》中所有博士的同伴最终背叛了他的剧集,并简述背叛的原因。", repo: MyApp.Repo, collection: "doctor-who", # 指定搜索的知识集合 max_iterations: 5 # 限制最大迭代次数,防止无限循环 ) # 2. 运行Loop,指定控制器和答案生成器模型 {:ok, final_ctx} = Arcana.Loop.run(ctx, controller_llm: "openai:gpt-4o-mini", # 负责决策的模型,要求快、便宜 answer_llm: "openai:gpt-4o" # 负责撰写最终答案的模型,可以更强、更贵 ) # 3. 分析结果 IO.puts("最终答案:#{final_ctx.answer}") IO.puts("循环终止原因:#{final_ctx.terminated_by}") # :answered, :gave_up, :max_iterations IO.inspect(final_ctx.tool_history) # 查看LLM调用工具的历史记录,例如:[:search, :search, :answer]Loop的内部状态机:
- 初始化:创建初始上下文,包含用户问题、可用工具集(
search,answer,give_up)的定义、迭代计数器等。 - 循环迭代: a. 将当前上下文(包括历史对话、之前的检索结果)格式化后,发送给控制器LLM。 b. 控制器LLM根据理解,决定调用哪个工具,并生成符合工具调用格式的响应。 c. Loop执行被调用的工具: -
search: 执行一次检索,将结果以摘要形式加入上下文。 -answer: 控制器认为信息已足够,触发答案生成器LLM,基于所有累积的上下文生成面向用户的最终答案,并终止循环。 -give_up: 控制器认为无法回答,终止循环。 d. 更新迭代计数。如果达到max_iterations,则触发回退合成:直接用答案生成器LLM基于已收集的上下文生成一个答案,然后终止。 - 终止:循环结束,返回最终的上下文,其中包含答案、工具调用历史和终止原因。
5.2 控制器提示工程与工具定义
Loop的性能高度依赖于控制器LLM的提示词(System Prompt)和工具定义。Arcana内置的提示词经过了精心设计,但了解其原理有助于你进行微调。
工具定义的精髓:Anthropic关于“为智能体编写工具”的指南深深影响了Arcana的设计。工具描述不仅要说明“它能做什么”,更要说明“什么时候不该用它”。例如,search工具的描述会强调:“仅当当前信息不足以直接回答问题,且你认为进一步搜索可能找到相关信息时才使用。如果你已经拥有了回答问题所需的所有关键信息,请使用answer工具。”
双模型策略:这是Loop模式的一个最佳实践。使用一个快速、廉价的模型(如gpt-4o-mini,claude-3-haiku)作为控制器,因为它需要在多轮迭代中频繁被调用,成本敏感。而最终答案的生成,则交给一个更强大、更昂贵的模型(如gpt-4o,claude-3-opus),它只被调用一次,用于产出高质量的回答。这样在效果和成本间取得了平衡。
集合选择与锁定:在Loop.new/2时,你可以传入多个collections。控制器LLM在每次调用search工具时,可以自主决定搜索哪个集合。这适用于跨领域知识库。如果你传入单个collection,则工具定义中不会包含集合参数,从而“锁定”搜索范围,这是一种安全约束。
5.3 实战案例:多跳问答模拟
假设我们有一个包含公司产品文档(集合products)和客户支持历史(集合support)的知识库。用户问:“客户A反映的关于产品B的某个界面卡顿问题,最后是怎么解决的?”
- 第一跳:控制器LLM可能首先在
products集合中搜索“产品B的界面卡顿常见原因”,获取一些背景知识。 - 第二跳:基于初步信息,它意识到需要具体的案例,于是在
support集合中搜索“客户A 产品B 卡顿”。 - 第三跳:检索到的支持记录提到了一个解决方案编号,控制器可能再次在
products或内部知识库(如果存在)中搜索该解决方案的详细步骤。 - 最终回答:控制器认为信息已完备,调用
answer工具,由答案生成器综合所有信息,生成给用户的答复。
这个过程完全由LLM自主驱动,无需开发者预先编写复杂的“如果-那么”规则。Loop的tool_history会清晰记录下[:search, :search, :search, :answer]这样的路径,提供了可解释性。
注意事项:Agentic Loop虽然强大,但也更不可预测且成本更高。务必设置
max_iterations(通常3-5轮足够)来限制成本。同时,密切监控tool_history和terminated_by,如果发现大量循环以:gave_up或:max_iterations结束,可能需要优化你的知识库内容、调整提示词或重新评估问题是否适合用Loop解决。对于有明确路径的问题,使用Pipeline模式通常更高效、更可控。
6. 知识图谱增强检索(GraphRAG)深入解析
除了传统的向量检索,Arcana还集成了GraphRAG能力,通过构建和利用知识图谱来提升检索的深度和关联性。这尤其适合处理包含大量实体、关系以及需要深层推理的文档集。
6.1 GraphRAG 的构建流程
GraphRAG不是替代向量检索,而是对其的增强。它的构建完全自动化,发生在文档摄入阶段:
- 实体提取:当文档被摄入时,Arcana会使用一个NER(命名实体识别)模型(通过Bumblebee)扫描文本,识别出如人物、组织、地点、产品、概念等实体。
- 关系链接:系统会分析句子结构,尝试找出被提取实体之间的关系。例如,在句子“张三在Acme公司开发了项目Alpha”中,会建立“张三”-“工作于”-“Acme公司”和“张三”-“开发了”-“项目Alpha”的关系边。
- 社区检测:所有实体和关系构成一张大图。Arcana使用Leiden算法(一种高效的社区发现算法)对这张图进行聚类,将紧密相关的实体分组形成“社区”。例如,所有与“Phoenix框架”相关的实体(如“LiveView”、“Channel”、“Ecto”)可能会被分到同一个社区。
- 社区摘要生成:对于每个检测到的社区,Arcana会使用LLM为该社区内的所有实体和关系生成一段连贯的文本摘要。这个摘要捕获了该社区的核心主题和内部关联。
最终,这些实体、关系、社区和社区摘要都被作为节点和边存入图数据库(在Arcana中,通过Ecto Schema存储在PostgreSQL中,利用递归查询或扩展进行图遍历)。
6.2 图谱与向量的融合搜索
当用户发起一个查询时,Arcana可以执行“混合搜索”的增强版——向量、全文、图谱三重融合。
- 向量与全文检索:如前所述,先进行标准的混合检索,得到初始的相关文本块列表。
- 图谱检索:同时,对查询进行实体提取。识别出的实体被用来在图谱中进行查找。
- 实体节点检索:直接找到与查询实体相关的节点。
- 社区扩展检索:找到这些实体所属的社区,然后将整个社区的摘要文本作为额外的检索候选。这是GraphRAG的精华:即使查询没有直接提到某个概念,但如果这个概念与查询实体在同一个紧密社区内,它也可能被检索到。这解决了“词汇不匹配”和“长尾关联”问题。
- 结果融合:将从图谱检索中得到的“社区摘要”文本块,与向量/全文检索得到的文本块合并到一个候选池中。
- 重排序:最后,使用交叉编码器对这个更大的候选池进行统一的重排序,得出最终的最相关片段列表。
这种融合方式对于复杂、多跳的查询尤其有效。例如,查询“Elixir的创始人开发了哪些其他重要项目?”。向量检索可能直接找到关于“Elixir创始人José Valim”的片段。而图谱检索通过“José Valim”节点,关联到他所在的“Elixir生态”社区,该社区的摘要可能提及“他也积极参与了Phoenix框架和Ecto库的早期开发”,从而将“Phoenix”和“Ecto”作为相关信息检索出来,即使原查询并未提及这两个词。
6.3 配置与启用GraphRAG
GraphRAG功能在Arcana中是可选的。要启用它,你需要在配置中启动图谱存储和图构建服务,并在摄入文档时开启相关选项。
# 在 config/config.exs 中配置图谱服务 config :arcana, :graph_store, module: Arcana.GraphStore.Postgres # 使用PostgreSQL作为图存储 # 在 supervision tree 中启动图构建服务(通常在 application.ex) children = [ # ... 其他服务 {Arcana.Graph.Builder, name: MyApp.GraphBuilder} ] # 在摄入文档时,启用实体提取和图构建 {:ok, doc} = Arcana.ingest(my_text_content, repo: MyApp.Repo, build_graph: true # 关键参数,开启图谱构建 )启用后,后续的搜索如果使用Arcana.search/2或Pipeline.search/1,且没有显式禁用,则会自动包含图谱融合的步骤。你也可以在Pipeline中精确控制:
ctx |> Pipeline.search(fuse_with_graph: true) # 明确启用图谱融合 |> ...性能考量:构建知识图谱会增加文档摄入阶段的开销,因为需要进行NER、关系提取和社区检测。对于大型文档集,建议在后台异步执行(Arcana路线图中的
Async ingestion via Oban正是为此)。在检索阶段,图谱融合也会增加一些查询复杂度,但通常带来的召回率提升是值得的。对于小型或领域狭窄的知识库,如果实体关系不复杂,可以评估是否真的需要GraphRAG。
7. 评估、监控与问题排查
构建RAG系统并非一劳永逸,其效果需要持续评估和优化。Arcana提供了从指标评估到实时监控的完整工具链。
7.1 构建评估体系
如何知道你的RAG系统表现好坏?你需要一个评估数据集和一套指标。
创建测试集:准备一组{问题, 标准答案, 相关文档ID}的元组。问题应覆盖你的常见查询类型。相关文档ID是“标准答案”所依据的源文档块,用于计算检索指标。
test_queries = [ %{ query: "Phoenix LiveView 是什么?", reference_answer: "一个用于构建实时、服务器渲染Web UI的Elixir库...", relevant_chunk_ids: ["chunk_123", "chunk_456"] # 这些ID来自你预先摄入的文档 }, # ... 更多测试用例 ]运行评估:Arcana提供了Arcana.Evaluation模块来辅助自动化评估。
results = Enum.map(test_queries, fn test_case -> {:ok, ctx} = Arcana.ask(test_case.query, repo: Repo, llm: "openai:gpt-4o-mini") %{ query: test_case.query, retrieved_ids: Enum.map(ctx.search_results, & &1.chunk_id), # 实际检索到的ID answer: ctx.answer, reference_answer: test_case.reference_answer, relevant_ids: test_case.relevant_chunk_ids } end)计算关键指标:
- 检索阶段指标:
- 召回率 (Recall@K):在前K个检索结果中,包含了多少比例的标准相关文档。
Recall@1和Recall@5是常用指标。 - 平均倒数排名 (MRR):对所有问题,计算标准答案第一次出现的位置的倒数,然后取平均。这个指标对排名靠前的结果更敏感。
- 召回率 (Recall@K):在前K个检索结果中,包含了多少比例的标准相关文档。
- 生成阶段指标:
- 忠实度 (Faithfulness):使用
Pipeline.ground/2或类似NLI模型计算生成的答案在多大程度上忠实于检索到的上下文,而非“幻觉”。 - 答案相关性 (Answer Relevance):评估生成的答案是否直接回答了问题。这通常需要LLM-as-a-judge或人工评估。
- 忠实度 (Faithfulness):使用
Arcana的Dashboard内置了运行评估任务和可视化这些指标的基础功能。
7.2 利用Telemetry进行监控
Arcana深度集成了Elixir的:telemetry库。几乎所有重要操作都会发出事件,让你可以实时监控系统健康状态和性能。
关键事件示例:
[:arcana, :embedder, :encode, :start]/[:arcana, :embedder, :encode, :stop]:嵌入模型调用耗时。[:arcana, :search, :start]/[:arcana, :search, :stop]:单次搜索总耗时。[:arcana, :pipeline, :step, :start]/[:arcana, :pipeline, :step, :stop]:每个Pipeline步骤的耗时,附有步骤名。[:arcana, :llm, :call, :start]/[:arcana, :llm, :call, :stop]:每次LLM API调用的耗时和令牌使用情况。
你可以将这些事件接入你的监控系统(如LiveDashboard、Telemetry.Metrics + Prometheus + Grafana)。
# 示例:在应用启动时附加一个简单的日志处理器 :telemetry.attach( "arcana-logger", [:arcana, :search, :stop], fn _event_name, measurements, _metadata, _config -> IO.inspect("Search completed in #{measurements.duration}ms") end, nil )7.3 常见问题与排查技巧
问题1:检索结果不相关
- 检查嵌入模型:你使用的嵌入模型是否与你的文档领域匹配?通用模型在处理高度专业术语时可能效果不佳。考虑使用领域内微调的模型,或尝试Arcana支持的
E5系列模型(它们支持在查询前添加指令前缀)。 - 调整分块策略:默认分块大小和重叠可能不适合你的文档。Arcana的Chunker是可配置的。对于技术文档,较小的块(如256字符)可能更好;对于叙事性内容,较大的块可能更合适。
- 优化混合搜索权重:
Arcana.search/2默认对向量和全文搜索结果进行RRF融合。你可以调整两者的权重(vector_weight和fulltext_weight),如果你的文档关键词特征明显,可以适当提高全文搜索的权重。
问题2:LLM生成的答案出现“幻觉”
- 启用并检查Grounding步骤:确保在Pipeline中包含了
Pipeline.ground/2步骤,并监控其输出的grounding.score。如果分数持续偏低,说明检索到的上下文不足以支撑生成,或者LLM的指令需要加强。 - 优化提示词:Arcana的
answer步骤有默认的提示词,但你可以在调用Pipeline.answer/2时传入自定义的prompt或prompt_fn。在提示词中强烈要求模型“严格基于提供的上下文回答,如果上下文没有相关信息,就说不知道”。 - 增加检索数量:尝试增加
search步骤返回的候选数量(top_k参数),为重排序和生成提供更多素材。
问题3:系统响应慢
- 定位瓶颈:使用Telemetry数据。是嵌入慢?检索慢?还是LLM调用慢?嵌入慢可以尝试启用GPU(EXLA)或使用更小的模型。检索慢可以检查数据库索引(确保
embedding向量列有ivfflat或hnsw索引)。LLM调用慢可以考虑使用更快的模型,或为API调用设置合理的超时和重试。 - 启用缓存:对于频繁出现的相同或相似查询,可以考虑在应用层实现一个简单的缓存,缓存
查询向量 -> 检索结果的映射。 - 异步处理:对于文档摄入等后台任务,务必使用异步作业(如Oban),避免阻塞实时请求。
问题4:Agentic Loop陷入无限循环或无效搜索
- 审查工具调用历史:查看
ctx.tool_history。如果看到大量连续的search而无answer,可能是控制器LLM无法从搜索结果中合成答案。 - 优化控制器提示词:确保工具描述清晰,特别是“何时停止搜索”的规则。可以尝试在系统提示中强调“如果你在连续两次搜索后没有得到新的关键信息,你应该考虑使用
answer或give_up”。 - 提供更好的搜索结果摘要:Arcana在给控制器LLM反馈搜索结果时,不是返回原始文本块,而是生成一个简洁的摘要。确保这个摘要信息量足够,能帮助LLM判断价值。
Arcana将强大的RAG能力无缝地带入了Elixir和Phoenix的世界。它从设计上就拥抱了BEAM平台的特性——并发、容错、内置可观测性,让你能用熟悉的工具和范式构建智能应用。无论是通过简单的ask/2快速上线一个问答机器人,还是利用Pipeline构建复杂的文档处理流程,或是探索前沿的Loop实现自主检索智能体,Arcana都提供了坚实且灵活的基础。更重要的是,它坚持“本地优先”和“一体化”的理念,在追求功能强大的同时,也极大地简化了运维和降低了长期成本。对于任何希望在其Elixir应用中深度集成智能检索与生成能力的团队来说,Arcana都是一个值得深入研究和采用的关键库。
