RAGSpine 用法 & Dify 流程迁移手册

把可视化工作流改成纯 Python · 重点路线:数字 + 归因混合(composite)· 代码均经实跑验证

framework-free 反幻觉 / 拒答 来源血缘强制 离线可验证

目录

  1. 30 秒认知
  2. 主要用法(三层级)
  3. 心智模型:三路分流
  4. Dify → 代码:概念对照
  5. "有状态多 agent" 与边界
  6. 节点映射表
  7. 实操:composite 迁移
  8. 完整脚本(可复制)
  9. Mock vs 真实模型
  10. 迁移 4 步 & 上线清单

030 秒认知

RAGSpine 不是一个你"提交给它"的框架,而是一个你用普通 Python 把零件拼起来的后端 RAG 引擎。

它有两条被代码强制(不是写在提示词里求模型听话)的铁律:

适用边界(先对号入座)
它是窄领域工具:带拒答和引用的"数字 + 叙事"问答。不是通用 agent 编排器。你的流程若是单轮问答(查数字 / 查归因 / 两者都要)——正中靶心;若是多步、多 agent、靠模型临场决定走向的复杂图——见 第 4 节的边界说明。

1主要用法(三层级,从浅到深)

层级 1 · 命令行(最快验证,离线无需 key)

ragspine quickstart                 # 10 秒离线演示:一个命中(带来源) + 一个诚实"查不到"
ragspine ask --db data/fact_metric.db "中国内地FY2024的REVENUE是多少"

mock 模式完全离线、确定性,不需要任何 API key。先跑 scripts/run_demo.py 生成示例库。

层级 2 · Python API(核心 — 记住 4 个名字就够)

整个最小可用 API 就是这四个名字:Fact / FactStore / MockProvider / answer_question

from ragspine.storage.fact_store import Fact, FactStore
from ragspine.agent.llm_provider import MockProvider
from ragspine.agent.agent import answer_question

# 1. 事实库(:memory: 临时;传路径就是真 sqlite)
store = FactStore(":memory:"); store.init_schema()

# 2. 一条事实 —— 最后两个字段是它的"血缘"(来自哪个文档、文档里哪个位置)
store.upsert_facts([Fact(
    metric_code="REVENUE", entity="ACME_CN", geography="CN", channel="TOTAL",
    period_type="FY", period="2024", value=1320.0, unit="USD_M",
    source_doc_id="ACME_FY2024_Results.pptx", source_locator="slide=6,table=1,row=2,col=3",
    tags={}, dimensions={},
)])

# 3. 离线确定性 provider(无 key、无网络);上线换 AnthropicProvider 即可
provider = MockProvider()

# 4. 提问。命中 → 带来源回答;缺失 → 诚实拒答,绝不编造
result = answer_question("中国内地FY2024的REVENUE是多少", store, provider)
print(result.answer)    # 确定值 + 来源,或诚实的"查不到"
print(result.sources)   # [{'doc':..., 'locator':...}]

answer_question(question, store, provider) 就是整个编排入口——意图解析 → 安全门 → 澄清 → 三路分流 → 工具循环 → 反幻觉兜底,全部藏在这一个函数里,你不需要画任何图。

层级 3 · HTTP 服务(异步摄取)

RAGSPINE_DB_PATH=data/fact_metric.db python scripts/run_server.py --port 8000   # FastAPI
python scripts/run_worker.py                                                     # RQ worker(需 Redis)

端点:POST /v1/ask · POST /v1/ingest/structured/jobs · POST /v1/ingest/narrative/jobs · GET /v1/jobs/{id}

2心智模型:三路分流

answer_question 内部对每个问题确定性地(规则,不是模型决定)选一条路:

路由问什么怎么答
structured"数字是多少"(营收/利润/ROE…)fact_metric 表 → found / not_found / unrecognized。数字只取自事实值,模型散文不被信任。
narrative"为什么 / 发生了什么"混合检索(BM25 + 向量 + RRF)→ 重排 → 带引用合成。
composite既要数字又要归因 ← 你的场景两路并行跑,合并:数字段确定性合成 + 归因段带引用。

3Dify → 代码:先建立关键认知

Dify 和 RAGSpine 不是一类东西,这决定了"迁移"到底是在做什么:

所以"把 Dify 流程改成代码" = 把可视化 DSL 翻译成 answer_question 调用 + 必要时你自己写的几行 Python 串联

4"有状态的多 agent" 是什么 & 什么时候不要硬搬

这是判断你的流程能否直接用 RAGSpine 的关键术语,拆成几个词理解:

含义
agent一个能自己决定下一步干什么的 LLM 循环:感知 → 决策 → 调工具 → 看结果 → 再决策,走向由模型临场判断。
多 agent多个这样的 agent 协作:一个"规划者"把任务拆给几个"执行者",或 agent 之间互相对话、移交任务(handoff)。
有状态流程在多步/多轮间要记住中间状态(对话历史、累积结果、轮到谁)。状态在节点间流转、被不断更新,还能中途暂停等人工(human-in-the-loop)、回溯、断点续跑。
这些步骤被组织成有向图(节点=步骤,边=跳转,条件边=按状态分流),由框架(如 LangGraph)驱动。

对照 RAGSpine:它是无状态的单次问答——进一个问题、出一个答案。路由是确定性规则(不是模型决定跳哪条边),不保留跨请求记忆,不能中途停下等人。它故意不做多 agent 图——一旦把路由交给模型决定,就破坏了"可证明的确定性拒答"这个核心卖点。

⚠️ 什么时候不要硬搬到 RAGSpine
如果你的 Dify 流程是这样——"LLM 分类 → 按判断走不同分支 → 各分支调不同工具 → 汇总 → 反问用户 → 带着前几轮记忆继续",走向靠模型临场决定、还要记住前面发生过什么——那它属于有状态多 agent 图,RAGSpine 做不了通用编排;你得用普通 Python 写编排骨架,RAGSpine 只负责其中"RAG 问答"那一段。
✅ 你的场景(数字 + 归因)不属于排除项
它是单轮、规则路由、无需记忆——正好对应 RAGSpine 的 composite 路由,可以放心直接迁。下面是完整步骤。

5节点映射表

Dify 节点RAGSpine 对应说明
知识检索 / Knowledge Retrievalbuild_narrative_retriever混合检索内置,你不用配。
LLM 节点 + 系统提示provider + 内置 prompt系统 prompt 由公司 profile 派生,不硬编码。
工具 / 函数调用(查数字)内置 query_metric开箱即用,无需自己 bind。
问题分类器 / 条件分支内置三路分流answer_question 自动做,无需画分支。
变量聚合 / 引用拼接result.sources引用是代码强制的不变量。
"找不到就说不知道"的提示词反幻觉硬约束(控制流)Dify 里靠提示词祈求,这里靠代码兜底。
应用配置存在 Postgres一段 .py 文件可读、可版本控制、可 diff。

6实操:composite(数字 + 归因)迁移

6.1 什么样的问题会走到 composite

路由判定就三行(agent/intent.py),composite 要同时满足两个条件:

中国内地FY2024的REVENUE是多少,为什么会有这样的变化   → composite ✅
中国内地FY2024的营收是多少                          → structured(只有指标,没叙事词)
中国内地业绩为什么下滑                               → narrative(只有叙事词,没指标)
⚠️ 最容易踩的坑
用户改个措辞少了"为什么 / 原因",问题就退回单路了。如果你希望无论措辞都跑两路,在你自己的入口层固定追加归因诉求,或直接分别调结构化与叙事两段再自行拼接。

6.2 灌叙事数据的最小 API(3 步)

build_narrative_retriever 只读不写,写入要先走 chunking:

from ragspine.retrieval.chunking.chunking import DocumentMeta, chunk_document
from ragspine.retrieval.chunking.chunk_store import ChunkStore

meta = DocumentMeta(doc_id=..., title=..., entity=..., geography=..., period=...,
                    language="zh", sensitivity="INTERNAL",   # 切勿用 RESTRICTED,会被检索出口剔除
                    source_locator_prefix=...)
chunks = chunk_document(text, meta)          # 段落贪心切块,每块自带 source_locator
ChunkStore(db).replace_doc_chunks(doc_id, chunks, valid_as_of="2025-03-31")  # 幂等版本化写入

7完整脚本(已实跑通过,直接复制运行)

从仓库根目录用 .venv/bin/python 这个脚本.py 运行。完全离线、无 key。

"""RAGSpine composite 路由端到端最小示例(纯 Python,离线,无 key)。"""
import tempfile
from datetime import date
from pathlib import Path

from ragspine.agent.agent import answer_question
from ragspine.agent.llm_provider import MockProvider
from ragspine.retrieval.chunking.chunking import DocumentMeta, chunk_document
from ragspine.retrieval.chunking.chunk_store import ChunkStore
from ragspine.retrieval.link.narrative_link import build_narrative_retriever
from ragspine.storage.fact_store import Fact, FactStore

tmp = Path(tempfile.mkdtemp(prefix="ragspine_demo_"))
fact_db, chunk_db = tmp / "facts.db", tmp / "chunks.db"

# a. 数字侧:写一条事实。注意 Fact 字段全部必填(tags/dimensions 也要显式传 {})
fact_store = FactStore(fact_db); fact_store.init_schema()
fact_store.upsert_facts([Fact(
    metric_code="REVENUE", entity="ACME_CN", geography="CN", channel="TOTAL",
    period_type="FY", period="2024",            # 注意是 "2024" 不是 "FY2024"
    value=128.6, unit="亿元",
    source_doc_id="FY2024_annual_report.pptx", source_locator="p.12",
    tags={}, dimensions={},
)])

# b. 叙事侧:切块灌入归因文本(带来源)
doc_id = "FY2024_mdna.pptx"
meta = DocumentMeta(doc_id=doc_id, title="FY2024 中国内地业务经营回顾",
                    entity="ACME_CN", geography="CN", period="2024", language="zh",
                    sensitivity="INTERNAL", source_locator_prefix=doc_id)
text = ("FY2024 中国内地营收同比增长主要由三方面驱动:\n"
        "一是代理人渠道产能提升,活动率与人均件数双升;\n"
        "二是银保渠道与头部银行的独家合作贡献了新增长极;\n"
        "三是续期业务因前期高质量新单而保持高留存。")
cs = ChunkStore(chunk_db); cs.init_schema()
cs.replace_doc_chunks(doc_id, chunk_document(text, meta), valid_as_of="2025-03-31")
cs.close()                                      # 写完即关,检索时重开同一个库

# c. 构造检索器(默认纯 BM25+RRF;传 provider 才挂 LLM 二审)
retriever, ret_store = build_narrative_retriever(chunk_db, provider=None)

# d. 提一个 composite 问题(既问数字又问归因)
provider = MockProvider(reference_date=date(2026, 6, 22))
result = answer_question(
    "中国内地FY2024的REVENUE是多少,为什么会有这样的变化",
    fact_store, provider,
    reference_date=date(2026, 6, 22),
    narrative_retriever=retriever,
)

print("route   :", result.route)
print("answer  :\n" + result.answer)
print("sources :", result.sources)
ret_store.close(); fact_store.close()

真实运行输出

route   : composite
answer  :
ACME_CN FY2024 REVENUE:128.6 亿元(来源:FY2024_annual_report.pptx · p.12)

归因分析:
基于检索到的资料: …(检索到的归因片段,带来源)
sources : [{'doc':'FY2024_annual_report.pptx','locator':'p.12'},
           {'doc':'FY2024_mdna.pptx','locator':'FY2024_mdna.pptx#para1-4'}]

composite 把两路结果拼接:数字段是确定性合成的"实体 期间 指标:值 单位(来源…)",归因段以"归因分析:"起头;result.sources 是两路来源之和。

⚠️ Fact 字段坑(实测踩过)

8MockProvider vs 真实 Claude(关键差异)

MockProvider(离线)AnthropicProvider(真实 Claude)
数字段两者完全一致——数字永远取自 fact 值,模型散文不参与(反幻觉硬约束)。
归因段离线占位:把检索到的片段原样回显(证明检索链通了、来源带上了),不会真总结。基于检索片段输出真正的归因散文,并强制带来源引用。
推荐迁移姿势
先用 MockProvider 跑通管线、写测试(离线无 key、确定性),确认行为对了;上线再换 AnthropicProvider(装 [llm] extra + API key)出真归因。叙事路径反幻觉策略与结构化不同:信任模型散文但强制来源引用(来源没出现在回答里就自动补"(资料来源:…)")。
from ragspine.agent.llm_provider import AnthropicProvider
provider = AnthropicProvider(model="claude-...", base_url="https://你的企业网关")  # 需装 [llm] extra
# 其余代码不变:answer_question(question, fact_store, provider, narrative_retriever=retriever)

9迁移 4 步 & 上线清单

1
数字 → FactStore(带血缘)。Dify 里"数据库/知识库查询"节点的结构化数据搬这里;可手写 upsert_facts,也可用 RAGSpine 的 extraction 层从 xlsx/pptx/pdf 自动抽。
2
归因文档 → ChunkStore(chunk_document + replace_doc_chunks)。Dify 里"知识检索"节点的文档进这里。
3
整张图 → 一行 answer_question(...)。Dify 里"分类器 → 分支 → 检索 → LLM → 变量聚合"全部由这一个函数内部完成。
4
先 Mock 跑通+写测试,再换 Anthropic 出真归因。

上线前自检