maglev-rb 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.ja.md +618 -0
  4. data/README.md +533 -248
  5. data/README.zh-CN.md +457 -250
  6. data/lib/generators/maglev/install/install_generator.rb +20 -0
  7. data/lib/generators/maglev/upgrade_index_version/upgrade_index_version_generator.rb +27 -0
  8. data/lib/generators/maglev/upgrade_source_identity/upgrade_source_identity_generator.rb +52 -0
  9. data/lib/maglev/active_record_extension.rb +141 -24
  10. data/lib/maglev/adapters/faraday_client.rb +94 -0
  11. data/lib/maglev/adapters/faraday_embedding.rb +51 -0
  12. data/lib/maglev/adapters/faraday_generation.rb +49 -0
  13. data/lib/maglev/adapters/faraday_planner.rb +88 -0
  14. data/lib/maglev/answerer.rb +30 -12
  15. data/lib/maglev/chunker.rb +39 -4
  16. data/lib/maglev/configuration.rb +66 -1
  17. data/lib/maglev/content_source_graph.rb +17 -11
  18. data/lib/maglev/dependency_graph.rb +72 -13
  19. data/lib/maglev/embedding_adapter.rb +10 -0
  20. data/lib/maglev/hybrid_candidate_set.rb +25 -0
  21. data/lib/maglev/hybrid_coordinator.rb +112 -0
  22. data/lib/maglev/hybrid_result.rb +25 -0
  23. data/lib/maglev/index_diagnostics.rb +83 -0
  24. data/lib/maglev/index_identity.rb +70 -0
  25. data/lib/maglev/index_state.rb +9 -0
  26. data/lib/maglev/indexer.rb +185 -35
  27. data/lib/maglev/knowledge_config.rb +27 -5
  28. data/lib/maglev/knowledge_registry.rb +33 -0
  29. data/lib/maglev/planner.rb +172 -0
  30. data/lib/maglev/planner_adapter.rb +25 -0
  31. data/lib/maglev/planner_evaluation.rb +49 -0
  32. data/lib/maglev/query_compiler.rb +197 -0
  33. data/lib/maglev/query_ir.rb +143 -0
  34. data/lib/maglev/query_validator.rb +311 -0
  35. data/lib/maglev/railtie.rb +9 -0
  36. data/lib/maglev/registry.rb +72 -0
  37. data/lib/maglev/reindex_job.rb +34 -2
  38. data/lib/maglev/relation_order.rb +16 -0
  39. data/lib/maglev/request.rb +22 -0
  40. data/lib/maglev/request_executor.rb +101 -0
  41. data/lib/maglev/resource_config.rb +222 -0
  42. data/lib/maglev/response.rb +2 -2
  43. data/lib/maglev/result.rb +30 -0
  44. data/lib/maglev/retrieval_outcome.rb +52 -0
  45. data/lib/maglev/retrieval_result.rb +25 -0
  46. data/lib/maglev/retriever.rb +282 -27
  47. data/lib/maglev/router.rb +77 -0
  48. data/lib/maglev/routing_adapter.rb +25 -0
  49. data/lib/maglev/schema_compiler.rb +17 -4
  50. data/lib/maglev/schema_snapshot.rb +159 -0
  51. data/lib/maglev/search_result.rb +7 -3
  52. data/lib/maglev/snapshot.rb +21 -1
  53. data/lib/maglev/snapshot_budget.rb +57 -0
  54. data/lib/maglev/snapshot_builder.rb +89 -11
  55. data/lib/maglev/source_extractor.rb +43 -0
  56. data/lib/maglev/source_fragment.rb +9 -0
  57. data/lib/maglev/structured_answer_composer.rb +67 -0
  58. data/lib/maglev/structured_evidence_builder.rb +56 -0
  59. data/lib/maglev/structured_executor.rb +157 -0
  60. data/lib/maglev/structured_result.rb +97 -0
  61. data/lib/maglev/trace.rb +56 -0
  62. data/lib/maglev/vector_stores/base.rb +12 -0
  63. data/lib/maglev/vector_stores/document.rb +14 -5
  64. data/lib/maglev/vector_stores/document_id.rb +27 -0
  65. data/lib/maglev/vector_stores/memory.rb +68 -6
  66. data/lib/maglev/vector_stores/metadata_filter.rb +55 -0
  67. data/lib/maglev/vector_stores/pgvector.rb +94 -7
  68. data/lib/maglev/version.rb +1 -1
  69. data/lib/maglev-rb.rb +3 -0
  70. data/lib/maglev.rb +36 -3
  71. data/lib/tasks/maglev.rake +43 -0
  72. metadata +71 -11
  73. data/lib/maglev/adapters/ruby_llm_attachment_extractor.rb +0 -15
  74. data/lib/maglev/adapters/ruby_llm_embedding.rb +0 -22
  75. data/lib/maglev/adapters/ruby_llm_generation.rb +0 -22
  76. data/lib/maglev/adapters/ruby_llm_provider.rb +0 -64
data/README.zh-CN.md CHANGED
@@ -1,48 +1,93 @@
1
1
  # Maglev
2
2
 
3
- [English](README.md) | [简体中文](README.zh-CN.md)
3
+ [English](README.md) | [简体中文](README.zh-CN.md) | [日本語](README.ja.md)
4
4
 
5
5
  [![CI](https://github.com/benjis/maglev/actions/workflows/ci.yml/badge.svg)](https://github.com/benjis/maglev/actions/workflows/ci.yml)
6
- [![Ruby](https://img.shields.io/badge/Ruby-3.2%2B-CC342D.svg)](https://www.ruby-lang.org/)
6
+ [![Ruby](https://img.shields.io/badge/Ruby-3.3%2B-CC342D.svg)](https://www.ruby-lang.org/)
7
7
  [![Rails](https://img.shields.io/badge/Rails-7.1%20%7C%208.0-D30001.svg)](https://rubyonrails.org/)
8
8
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.txt)
9
9
 
10
- **无需在 Rails 之外另建一套系统,也能让 Rails 模型拥有语义记忆。**
10
+ Maglev 0.2 是面向 ActiveRecord 应用的 Rails 原生只读知识与查询层。应用可以通过三条明确路线回答自然语言问题:
11
11
 
12
- Maglev 是面向 ActiveRecord 对象图的 Rails 原生语义知识层。你只需声明领域模型中哪些信息适合被理解和检索,Maglev 就会把记录、关联关系、附件和富文本转换为可搜索的知识。它通过 Rails 原有的生命周期机制持续更新这些知识,并以熟悉的模型 API 提供语义搜索和有依据的问答能力。
12
+ - **结构化查询:** 问题 经过验证的 Query IR 可组合的
13
+ `ActiveRecord::Relation` 或有界聚合值。
14
+ - **RAG:** 问题 → 经过授权的语义检索 → 可选的有据回答。
15
+ - **混合查询:** 用两种固定流程之一组合结构化筛选与 RAG 证据。
16
+
17
+ 模型只暴露应用显式声明的 allowlist。结构化编译始终从调用方提供的 base relation 开始,并且只能继续收窄。RAG 检索与答案生成彼此独立。
13
18
 
14
19
  ```ruby
15
- Product.search("存在续航或易用性问题的笔记本电脑")
20
+ resource_authorizer = ->(_entry, user) { user.account_id == current_account.id }
21
+ result = current_account.invoices.maglev_request(
22
+ "金额超过 500 美元的未结发票",
23
+ mode: :structured,
24
+ planner_adapter: planner,
25
+ authorizer: resource_authorizer,
26
+ user: current_user
27
+ )
16
28
 
17
- response = Product.ask("有哪些反复出现、值得调查的产品问题?", user: current_user)
18
- response.text
19
- response.sources # 支撑回答的 ActiveRecord 记录和内容分块
29
+ retrieval = SupportTicket.retrieve("取消流程中受阻的客户", user: current_user)
30
+ answer = SupportTicket.ask("反复出现了哪些取消问题?", user: current_user)
20
31
  ```
21
32
 
22
- Maglev 专注于检索增强生成(RAG)。ActiveRecord 仍然负责精确筛选、关联查询、报表和聚合;Maglev 则处理用自然语言提出的问题。
33
+ ## 从这里开始:先选择数据路径
34
+
35
+ 第一次使用 Maglev 时,不需要先理解完整架构。先判断问题需要哪种数据:
36
+
37
+ | 用户问题 | 所需数据 | 声明 | 调用方式 |
38
+ | --- | --- | --- | --- |
39
+ | “有多少发票已经逾期?” | 精确字段、筛选、计数 | `queryable` | `maglev_request(..., mode: :structured)` |
40
+ | “客户取消服务时主要抱怨什么?” | 正文、评论、附件等自由文本 | `knowledge` | `retrieve` 或 `ask` |
41
+ | “哪些未关闭工单提到了重复扣款?” | 精确状态 + 语义文本 | 两者都声明 | `maglev_request(..., mode: :hybrid)` |
42
+
43
+ 只需要记住四件事:
44
+
45
+ 1. `maglev_resource :support_tickets` 给模型一个稳定的 Maglev 资源名。
46
+ 2. `queryable` 是结构化查询 allowlist:规划器只能筛选、排序、关联或聚合这里声明的内容。
47
+ 3. `knowledge` 选择哪些内容会变成可语义检索的证据。
48
+ 4. 授权仍由应用提供;结构化查询还必须从调用方的
49
+ `ActiveRecord::Relation` 开始。Maglev 不会自行扩大权限。
23
50
 
24
51
  ## 为什么选择 Maglev?
25
52
 
26
- - **Rails 原生:** 它是一个 gem Railtie,而不是独立服务、Engine 或额外 API
27
- - **模型驱动:** 在拥有数据的 ActiveRecord 模型旁直接声明知识边界。
28
- - **理解对象图:** 支持直接关联、`has_many :through` 和多态关联,并可显式限制深度与记录数。
29
- - **自动保持新鲜:** 已声明记录、直接关联、附件或 Action Text 内容发生变化后,自动重新索引知识所有者。
30
- - **回答有依据:** 仅根据检索到的上下文生成答案,并返回来源。
31
- - **面向生产环境:** 内置授权接口、内容限制、清洗、重试、可观测性和幂等重建索引。
32
- - **可扩展:** 可使用默认的 PostgreSQL/pgvector,也可实现精简的向量存储协议。
53
+ - 普通 Ruby gem + `Maglev::Railtie`,不是 Rails Engine 或独立 API 服务。
54
+ - ActiveRecord-first 结构化查询;不生成或执行不受限 SQL/Ruby。
55
+ - 显式注册可查询字段、关联、scope、聚合和知识来源。
56
+ - 由应用拥有的 relation 携带租户和授权约束。
57
+ - 基于 pgvector 的来源感知 RAG,具有归一化相似度、确定性预算和可检查证据。
58
+ - 不可变计划/结果和默认脱敏 trace。
59
+ - 默认测试使用确定性 fake adapter,不调用线上模型服务。
33
60
 
34
- ## 快速开始
61
+ ## 架构
35
62
 
36
- ### 1. 安装前置依赖
63
+ ```mermaid
64
+ flowchart TD
65
+ Q["问题 + 已注册范围"] --> R["路由器"]
66
+ R --> S["结构化规划器"]
67
+ R --> G["RAG 检索"]
68
+ R --> H["固定混合协调器"]
69
+ S --> I["不受信任的 Query IR"]
70
+ I --> V["确定性验证器"]
71
+ V --> C["在 base relation 上编译"]
72
+ C --> E["只读有界执行"]
73
+ G --> X["已授权语义证据"]
74
+ H --> E
75
+ H --> X
76
+ E --> O["证据 + Result"]
77
+ X --> O
78
+ O --> A["可选的有据答案"]
79
+ Q --> T["脱敏 trace"]
80
+ ```
37
81
 
38
- Maglev 需要 Ruby 3.2+、Rails 7.1 或 8.0、PostgreSQL,以及
39
- [`pgvector`](https://github.com/pgvector/pgvector) 扩展。
82
+ Registry 是权限边界。一次请求的 schema snapshot 只包含已注册且已授权的资源,绝不包含记录值。Provider 输出在通过确定性验证前一律不受信任。
40
83
 
41
- Maglev 加入应用:
84
+ ## 安装
85
+
86
+ Maglev 需要 Ruby 3.3+、Rails 7.1 或 8.0、PostgreSQL 和 pgvector。
42
87
 
43
88
  ```ruby
44
89
  # Gemfile
45
- gem "maglev-rb"
90
+ gem "maglev-rb", "~> 0.2.1"
46
91
  ```
47
92
 
48
93
  ```bash
@@ -51,358 +96,520 @@ bin/rails generate maglev:install --embedding-dimensions=1536
51
96
  bin/rails db:migrate
52
97
  ```
53
98
 
54
- 生成器会把 `--embedding-dimensions` 同时写入 `config/initializers/maglev.rb` `maglev_chunks` 的向量列,并创建基于余弦距离的 HNSW 索引。
99
+ Generator 会创建 initializer、`maglev_chunks`、来源/租户元数据、HNSW 余弦索引和 `maglev_index_states` 诊断表。Owner 使用 UUID 主键时请检查并调整生成的迁移。
100
+
101
+ ## 配置
55
102
 
56
- ### 2. 配置模型提供商
103
+ 内置 embedding 和 generation 客户端使用 OpenAI-compatible HTTP 协议。Planner 默认使用 OpenAI 的 `json_schema` response format;不支持该能力的服务商可以改用约束较弱的 `json_object` format。Embedding 与 generation 可以使用不同服务商。
57
104
 
58
105
  ```ruby
59
- # config/initializers/maglev.rb
60
106
  Maglev.configure do |config|
61
107
  config.embedding_provider do |provider|
62
- provider.url = "http://localhost:11434/v1"
63
- provider.api_key = ENV["LOCAL_EMBEDDING_API_KEY"]
64
- provider.model = "Qwen3-Embedding-0.6B-8bit"
65
- provider.dimensions = 1024
108
+ provider.url = ENV.fetch("MAGLEV_EMBEDDING_URL", "https://api.openai.com/v1")
109
+ provider.api_key = ENV["MAGLEV_EMBEDDING_API_KEY"]
110
+ provider.model = "text-embedding-3-small"
111
+ provider.dimensions = 1536
66
112
  end
67
113
 
68
114
  config.generation_provider do |provider|
69
- provider.url = "https://api.deepseek.com/v1"
70
- provider.api_key = Rails.application.credentials.dig(:deepseek_api_key)
71
- provider.model = "deepseek-chat"
115
+ provider.url = ENV.fetch("MAGLEV_GENERATION_URL", "https://api.openai.com/v1")
116
+ provider.api_key = ENV["MAGLEV_GENERATION_API_KEY"]
117
+ provider.model = "gpt-4.1-mini"
72
118
  end
73
119
 
120
+ config.planner_adapter = Maglev::Adapters::FaradayPlanner.new
121
+ # 用于不支持 json_schema 的服务商:
122
+ # config.planner_adapter = Maglev::Adapters::FaradayPlanner.new(response_format: :json_object)
123
+ config.routing_adapter = MyRoutingAdapter.new # 仅 mode: :auto 需要
124
+
74
125
  config.chunk_size = 1000
126
+ config.minimum_similarity = nil
127
+ config.retrieval_max_candidates = 1000
128
+ config.context_max_characters = 4000
129
+ config.context_per_owner_characters = 1200
130
+
131
+ config.snapshot_attribute_max_characters = 20_000
132
+ config.snapshot_related_record_max_characters = 50_000
133
+ config.snapshot_max_characters = 100_000
134
+ config.snapshot_max_chunks = 100
135
+
136
+ config.structured_query_timeout = 5
137
+ config.structured_evidence_max_rows = 100
138
+ config.structured_evidence_max_bytes = 32_768
75
139
  end
76
140
  ```
77
141
 
78
- Embedding generation endpoint 相互独立,可以使用不同的 URL、API key 和 model。默认 provider bridge 使用 OpenAI-compatible HTTP endpoint;其他协议仍可通过 Maglev 自定义 adapter 接入。
142
+ 若内置协议不适用,可以注入自定义 `embedding_adapter`、`generation_adapter`、`planner_adapter`、`routing_adapter`、`attachment_extractor` `authorization_adapter`。
143
+
144
+ ## 注册资源
79
145
 
80
- 对于已有安装,请同时修改配置维度和数据库向量列。Maglev 会在请求 embedding 前检查两者是否一致。
146
+ `maglev_resource` v0.2 的主要 DSL。结构化查询能力与知识能力彼此独立,可以同时声明,也可以只声明一种。
81
147
 
82
- ### 3. 声明模型知识
148
+ ### 一个带完整注释的结构化资源
83
149
 
84
150
  ```ruby
85
- class Product < ApplicationRecord
86
- has_many :reviews, inverse_of: :product
87
- has_many :product_categories, inverse_of: :product
88
- has_many :categories, through: :product_categories
89
- has_many_attached :images
90
- has_rich_text :description
91
-
92
- has_knowledge do
93
- expose :name, :sku, :price, :status
94
- tags :product
95
-
96
- include_related :reviews, depth: 1, limit: 10
97
- include_related :categories, depth: 1, limit: 10, inverse: :products
98
-
99
- expose_attached :images
100
- expose_rich_text :description
151
+ class Invoice < ApplicationRecord
152
+ # Rails 关联和 scope 仍然是业务逻辑的事实来源。
153
+ belongs_to :account
154
+ scope :due_before, ->(date) { where(due_on: ..date) }
155
+
156
+ # :invoices 是 Maglev plan/request 使用的稳定资源标识符。
157
+ maglev_resource :invoices do
158
+ # 帮助 planner 理解这个资源代表什么。
159
+ description "属于已授权账户的发票"
160
+ synonyms "bills"
161
+
162
+ # 这个 block 只控制结构化 ActiveRecord 查询。
163
+ queryable do
164
+ # 允许对这些真实数据库字段执行精确筛选和排序。
165
+ # enum 同时限定 planner 可以使用的状态值。
166
+ field :status, enum: %w[draft open paid void]
167
+ field :amount, description: "以账户币种表示的发票总额"
168
+ field :due_on, synonyms: ["deadline", "截止日期"]
169
+ field :paid_at
170
+
171
+ # 即使 planner 请求,也明确禁止这些敏感字段。
172
+ prohibit :number, :internal_note
173
+
174
+ # 允许通过已注册关联连接到另一个 :accounts 资源。
175
+ association :account, resource: :accounts
176
+
177
+ # 允许调用这个已有 Rails scope,并定义其参数类型。
178
+ scope :due_before,
179
+ parameters: {date: {type: :date, required: true}}
180
+
181
+ # 只开放这些聚合函数和字段。
182
+ aggregates count: true, sum: [:amount], average: [:amount]
183
+
184
+ # 资源级上限,会与全局/请求级限制取最严格值。
185
+ limits rows: 50, operations: 8, joins: 1
186
+
187
+ # 每次结构化规划都必须由调用方授权该资源。
188
+ authorization :required
189
+ end
190
+
191
+ # 这个独立 block 控制语义索引和 RAG。
192
+ # 字段在这里重复出现,不会扩大结构化查询权限。
193
+ knowledge do
194
+ expose :status, :amount, :due_on, :paid_at
195
+ end
101
196
  end
102
197
  end
198
+ ```
103
199
 
104
- class Review < ApplicationRecord
105
- belongs_to :product, inverse_of: :reviews
200
+ Maglev 不会隐式暴露任何内容。`authorization :required` 表示:除非调用方明确授权,否则该资源不会进入请求的 schema snapshot。`allow_unscoped_model_queries` 必须显式启用,且只应供真正公开的数据使用。
106
201
 
107
- has_knowledge do
108
- expose :rating, :title, :body
109
- end
110
- end
111
- ```
202
+ `queryable` 只定义受约束的 ActiveRecord 查询契约;`knowledge` 只定义 RAG
203
+ 索引和检索来源;`maglev_resource` 是统一资源声明,可以只包含其中一个 block,
204
+ 也可以同时包含两者。未声明 `knowledge` 的模型不能使用 `search`、`retrieve`、
205
+ `ask`、snapshot 或索引 callback。
112
206
 
113
- 只有显式公开的字段和内容源才会进入 Maglev 的知识快照。关联的 `limit` 限制每个关联包含的记录数;`depth` 限制关联跳数:`depth: 1` 会包含直接关联记录,但不会继续展开该记录的关联。`config.max_relation_depth` 是每个快照从根记录到叶记录的全局硬上限。每个关联模型独立声明自己的公开知识,因此连接模型中的敏感字段不会被意外展开。
207
+ 请把声明理解为 allowlist,而不是数据库 schema 的复制。未出现在 `field` 中的列
208
+ 不能进入 Query IR;未出现在 `expose` 或其他 knowledge source 中的值不会进入语义
209
+ snapshot。
114
210
 
115
- ### 4. 为已有记录建立索引
211
+ ### 为什么一个字段可以同时出现在两个 block 中
116
212
 
117
- 新建和更新记录时会自动将 `Maglev::ReindexJob` 加入队列。安装后可执行一次已有数据回填:
213
+ 同一字段可以承担两种不同职责:
118
214
 
119
- ```bash
120
- bin/rails maglev:reindex[Product]
121
- # 或重建所有声明了 has_knowledge 的模型
122
- bin/rails maglev:reindex_all
123
- ```
215
+ - `field :status` 允许结构化查询生成 `status = "open"` 这样的精确条件。
216
+ - `expose :status` 会把 `status: open` 写入索引 snapshot,让检索证据保留上下文。
124
217
 
125
- 请确保生产环境中的 Active Job 后端正在运行。
218
+ 声明一边不会自动声明另一边。标识符、日期、枚举和金额适合放入 `queryable`;
219
+ 描述、正文、评论、解决记录和附件文本适合放入 `knowledge`;状态、优先级、产品区域
220
+ 这类上下文字段则经常需要同时声明。
126
221
 
127
- ### 5. 搜索与提问
222
+ ### 一个真正体现 RAG 价值的资源
128
223
 
129
- ```ruby
130
- results = Product.search(
131
- "存在续航或易用性问题的笔记本电脑",
132
- limit: 10,
133
- user: current_user
134
- )
224
+ 当答案存在于人写的语言中,而不是某个精确列里时,RAG 才真正有价值。下面的结构化
225
+ 查询可以找出 open/high-priority 工单,而 RAG 可以从工单正文、评论、解决记录和日志
226
+ 附件中理解“重复扣款”这类不同措辞。
135
227
 
136
- results.each do |result|
137
- result.owner # => Product
138
- result.content # 检索到的快照分块
139
- result.source # => "snapshot"
140
- result.distance # 余弦距离,越小越接近
141
- result.similarity # 归一化后的便捷相似度分数
228
+ ```ruby
229
+ class SupportTicket < ApplicationRecord
230
+ belongs_to :account
231
+ has_many :comments
232
+ has_many_attached :files
233
+ has_rich_text :resolution
234
+
235
+ maglev_resource :support_tickets do
236
+ description "客户支持请求及其调查证据"
237
+
238
+ queryable do
239
+ # 适合结构化查询:精确、有类型、便于筛选。
240
+ field :status, enum: %w[open pending resolved closed]
241
+ field :priority, enum: %w[low normal high urgent]
242
+ field :product_area
243
+ field :created_at
244
+ prohibit :requester_email, :internal_notes
245
+ limits rows: 100, operations: 8, joins: 1
246
+ authorization :required
247
+ end
248
+
249
+ knowledge do
250
+ # 正文包含精确筛选无法表达的语义。
251
+ expose :subject, :body
252
+
253
+ # 与 queryable 重叠,使证据保留业务上下文。
254
+ expose :status, :priority, :product_area
255
+
256
+ # 只纳入数量和顺序都确定的关联对话。
257
+ include_related :comments, depth: 1, limit: 20,
258
+ order: {created_at: :desc}
259
+
260
+ # 纳入受支持的附件文本和 Action Text 内容。
261
+ expose_attached :files
262
+ expose_rich_text :resolution
263
+ end
264
+ end
142
265
  end
143
266
  ```
144
267
 
145
- 根据检索到的记录生成有依据的回答:
268
+ 被 `include_related` 使用的关联模型也必须声明自己的
269
+ `maglev_resource ... knowledge`。只需要 RAG 时省略 `queryable`;只需要结构化查询时
270
+ 省略 `knowledge`。
271
+
272
+ 记录完成索引后,下面三个调用解决不同问题:
146
273
 
147
274
  ```ruby
148
- response = Product.ask(
149
- "商品团队应调查哪些反复出现的产品问题?",
275
+ # 只返回语义证据,不调用 generation provider。
276
+ evidence = SupportTicket.retrieve(
277
+ "客户说取消服务后仍被重复扣款",
278
+ limit: 10,
279
+ user: current_user
280
+ )
281
+
282
+ # 根据选中的工单证据生成有据可查的自然语言回答。
283
+ answer = SupportTicket.ask(
284
+ "取消服务后反复出现了哪些重复扣款模式?",
150
285
  limit: 5,
151
286
  user: current_user
152
287
  )
153
288
 
154
- response.text
155
- response.sources # 所有者、分块、距离和实际检索到的内容
156
- response.metadata
289
+ # 先按数据库字段精确筛选,再只在结果集合中做语义检索。
290
+ result = current_account.support_tickets.maglev_request(
291
+ "客户描述被重复扣款的未关闭紧急工单",
292
+ mode: :hybrid,
293
+ hybrid_plan: :structured_first,
294
+ planner_adapter: planner,
295
+ authorizer: resource_authorizer,
296
+ user: current_user
297
+ )
157
298
  ```
158
299
 
159
- 例如,回答可能根据检索到的评价内容,总结出风扇噪音、触控板偶尔失灵以及实际续航低于宣传值。应把它理解为对“已检索上下文”的总结,而不是对全部产品做出的数据库聚合;每项结论都应通过 `response.sources` 展示依据。
300
+ 默认附件提取器支持纯文本、Markdown、HTML XHTML。PDF、Office、OCR、图片、音视频解析需要应用自定义 extractor。Snapshot、relation、附件和 chunk 都有硬预算。
160
301
 
161
- 实例级问题会限定在单个知识所有者内:
302
+ 无需调用 provider 即可检查暴露内容:
162
303
 
163
304
  ```ruby
164
- product.ask("总结这款产品被反馈的优点和缺点。", user: current_user)
305
+ SupportTicket.maglev_schema
306
+ ticket.maglev_snapshot
307
+ ticket.maglev_context_preview(question: "为什么还未解决?")
308
+ ticket.maglev_index_status
165
309
  ```
166
310
 
167
- Rails 数据变化时,Maglev 还会沿已声明的关联更新知识。将评价移动到另一款产品后,事务提交会为原产品和新产品安排重新索引,使两边的可搜索知识保持最新:
311
+ ### DSL API 参考
168
312
 
169
- ```ruby
170
- review.update!(product: replacement_product)
171
- # 两个受影响的产品都会加入 Maglev::ReindexJob 队列。
172
- ```
313
+ 资源级 DSL:
173
314
 
174
- 如果没有检索到可用上下文,Maglev 会返回确定性的“上下文不足”响应,而不会让模型自行猜测。
315
+ | DSL | 用途 |
316
+ | --- | --- |
317
+ | `maglev_resource :identifier` | 为模型注册一个稳定的资源标识符。 |
318
+ | `description "..."` | 提供给 planner 的资源说明;不会包含记录值。 |
319
+ | `synonyms "...", "..."` | 资源可能使用的其他名称。 |
320
+ | `queryable { ... }` | 声明结构化 ActiveRecord 能力;只能出现一次。 |
321
+ | `knowledge { ... }` | 声明 RAG/索引能力;只能出现一次。 |
175
322
 
176
- ## 工作原理
323
+ `queryable` DSL:
177
324
 
178
- ```mermaid
179
- flowchart LR
180
- A["ActiveRecord 对象图"] --> B["显式知识结构"]
181
- B --> C["有边界且已清洗的快照"]
182
- C --> D["分块 + 嵌入"]
183
- D --> E["向量存储<br/>默认 pgvector"]
184
- Q["search / ask"] --> F["语义检索"]
185
- E --> F
186
- F --> G["上下文组装"]
187
- G --> H["基于上下文生成"]
188
- H --> I["回答 + 来源"]
189
- J["Rails 提交与内容变化"] --> K["Active Job 重建索引"]
190
- K --> C
191
- ```
325
+ | DSL | 用途与选项 |
326
+ | --- | --- |
327
+ | `field :name` | 允许一个真实字段。选项:`description:`、`synonyms:`、`enum:`、`sensitive:`。敏感字段不会进入 planner schema。 |
328
+ | `prohibit :a, :b` | 明确禁止真实字段;同一字段不能既允许又禁止。 |
329
+ | `association :account, resource: :accounts` | 允许一个已注册 ActiveRecord 关联路径。支持 `description:`、`synonyms:`;目标资源也必须注册。 |
330
+ | `scope :due_before, parameters: {...}` | 允许一个已有 model scope。参数 metadata 支持 `type`、`required`、`nullable`、`enum_values`、`minimum`、`maximum`。其他 scope 不能调用。 |
331
+ | `aggregates count: true, sum: [:amount]` | 允许 `count`、`sum`、`average`、`minimum`、`maximum`;字段列表进一步限制聚合目标。 |
332
+ | `limits rows:, operations:, joins:` | 设置正整数资源上限;最终使用所有配置中最严格的限制。 |
333
+ | `authorization :required` | 默认值。只有 `authorizer` 批准后,资源才进入 schema snapshot。 |
334
+ | `authorization :public` | 资源 schema 公开;记录访问仍受传入 relation 约束。 |
335
+ | `allow_unscoped_model_queries true` | 允许没有 base relation 的结构化请求。默认关闭,只应用于真正公开的数据。 |
336
+
337
+ Scope 参数 `type` 支持 `:string`、`:integer`、`:float`、`:decimal`、
338
+ `:boolean`、`:date`、`:datetime`、`:timestamp` 和 `:time`。注册资源时会拒绝不支持的类型。
192
339
 
193
- 1. `has_knowledge` 为模型及其声明的关联编译一份显式知识结构。
194
- 2. Maglev 根据允许的属性、关联记录、附件、富文本和标签生成确定性的文本快照。
195
- 3. 快照被拆成大小受限的分块,并通过配置的适配器生成嵌入向量。
196
- 4. 分块被写入向量存储;默认存储使用 PostgreSQL 和 pgvector。
197
- 5. `search` 为查询生成嵌入,并执行基于余弦距离的近邻检索。
198
- 6. `ask` 在上下文预算内组装分块,构建有依据的提示词,并返回包含来源元数据的答案。
199
- 7. Rails 回调会沿对象图传播已声明记录的变化,并为受影响的所有者安排重新索引。
340
+ `knowledge` DSL:
200
341
 
201
- Maglev 不会复制你的关系数据模型。向量文档会指回对应的 ActiveRecord 所有者;事务、业务规则和结构化查询仍由应用负责。
342
+ | DSL | 用途与选项 |
343
+ | --- | --- |
344
+ | `expose :subject, :body` | 把指定的非 nil 模型字段加入可搜索 snapshot。 |
345
+ | `hide :internal_notes` | 明确记录不得暴露的字段;同一字段不能同时 expose 和 hide。 |
346
+ | `tags :support, :customer` | 给该模型的每个 snapshot 添加固定分类标签。 |
347
+ | `include_related :comments, depth:, limit:` | 加入有界关联记录 snapshot。`inverse:` 可指定不明显的反向关联;`order:` 接受字段或 `{field: :asc/:desc}`。 |
348
+ | `expose_attached :files` | 加入指定 Active Storage 附件中提取出的文本。 |
349
+ | `expose_rich_text :resolution` | 加入指定 Action Text 字段的纯文本。 |
202
350
 
203
- ## 知识来源
351
+ Maglev 不会根据数据库可见性自动推断权限。DSL 会在注册时验证;未知字段、关联、
352
+ scope 或附件会立即抛出 `Maglev::ConfigurationError`。
204
353
 
205
- ### ActiveRecord 对象图
354
+ ## 结构化查询
206
355
 
207
- `include_related` 支持对普通关联、`has_many :through` 和多态关联进行有限遍历。当 Maglev 无法自动推断关联模型的变化应如何找到知识所有者时,可使用 `inverse:` 显式指定反向关联。
356
+ 规划与执行刻意分离。
208
357
 
209
358
  ```ruby
210
- has_knowledge do
211
- include_related :tickets, depth: 2, limit: 25
212
- include_related :events, depth: 1, limit: 20, inverse: :eventable
213
- end
359
+ base = current_account.invoices.where(archived: false)
360
+
361
+ plan = Maglev.plan(
362
+ "本月到期、金额超过 500 美元的未结发票",
363
+ resource: :invoices,
364
+ base_relation: base,
365
+ authorizer: ->(entry, user) { user.account_id == current_account.id },
366
+ user: current_user,
367
+ constraints: {rows: 25, operations: 8, joins: 1},
368
+ adapter: planner
369
+ )
370
+
371
+ plan.status # :ready / :clarification_required / :unsupported / :invalid
372
+ plan.ir # ready 时为不可变 Maglev::QueryIR::Query
373
+ plan.explanation
374
+ plan.policy_limits # 实际生效的 rows/operations/joins/complexity
375
+ plan.evidence_requirements
376
+ plan.trace_id
377
+
378
+ result = Maglev.execute(plan)
379
+ result.status # :succeeded
380
+ result.kind # :relation 或 :aggregate
381
+ result.value # 受保护的 relation 或有界 scalar
382
+ result.evidence
383
+ result.render
214
384
  ```
215
385
 
216
- 当关联记录从一个所有者转移到另一个所有者时,Maglev 会同时重新索引旧所有者和新所有者。
386
+ 记录 relation 在读取前保持 lazy 和可组合。读取发生在带 `statement_timeout` 的 PostgreSQL 只读事务中;返回记录只读,批量写操作会被拒绝。
387
+
388
+ Query IR v1 支持已注册 scope;`eq`、`not_eq`、`gt`、`gte`、`lt`、`lte`、`in`、`not_in`、`is_null`、`is_not_null`、`between`;最多两层 join;sort、distinct、limit;以及 count、sum、average、minimum、maximum。它不能包含 SQL、Arel、Ruby、任意方法、写操作、锁、嵌套布尔组、窗口函数、子查询、`HAVING` 或规划器自定义工具。
217
389
 
218
- 创建、删除或重新分配连接记录会改变 `has_many :through` 关系,但不会改变关联记录本身。此类连接模型变更后,请在应用中显式加入或执行所有者重新索引。
390
+ ## RAG:search、retrieve ask
219
391
 
220
- ### Active Storage 与 Action Text
392
+ 知识资源仍然可以直接使用模型 API。
221
393
 
222
394
  ```ruby
223
- has_knowledge do
224
- expose_attached :contracts, :brief
225
- expose_rich_text :notes
226
- end
395
+ matches = SupportTicket.search(
396
+ "取消流程故障",
397
+ limit: 10,
398
+ minimum_similarity: 0.65,
399
+ user: current_user
400
+ )
401
+
402
+ matches.first.owner
403
+ matches.first.source_identity
404
+ matches.first.source_type
405
+ matches.first.similarity
227
406
  ```
228
407
 
229
- HTML Action Text 内容会在索引前进行清洗。附件会受到内容类型、字节大小和提取字符数限制。已声明附件和富文本的变化会触发所有者重新索引。
408
+ `search` 每个 owner 最多返回一个 `SearchResult`。需要完整、无生成的检索诊断时使用 `retrieve`:
409
+
410
+ ```ruby
411
+ retrieval = SupportTicket.retrieve(
412
+ "取消流程故障",
413
+ limit: 10,
414
+ chunks_per_owner: 2,
415
+ user: current_user
416
+ )
230
417
 
231
- ### 在索引前检查内容
418
+ retrieval.considered
419
+ retrieval.selected
420
+ retrieval.rejected
421
+ retrieval.context
422
+ retrieval.budgets
423
+ retrieval.reasons
424
+ retrieval.timings
425
+ retrieval.trace_id
426
+ ```
232
427
 
233
- 以下开发者 API 可用于确认模型实际公开的内容,且不会调用嵌入或生成服务:
428
+ 只有需要自然语言答案时才调用生成:
234
429
 
235
430
  ```ruby
236
- Customer.maglev_schema
237
- customer.maglev_snapshot
238
-
239
- preview = customer.maglev_context_preview(
240
- question: "为什么这位客户存在风险?"
431
+ answer = SupportTicket.ask(
432
+ "哪些取消故障反复出现?",
433
+ limit: 5,
434
+ chunks_per_owner: 2,
435
+ minimum_similarity: 0.65,
436
+ user: current_user
241
437
  )
242
- preview.text
243
- preview.metadata # 包含 provider_calls: 0
438
+
439
+ answer.text
440
+ answer.sources
441
+ answer.metadata
244
442
  ```
245
443
 
246
- ## 授权
444
+ 若授权、相似度或上下文预算过滤掉全部证据,`ask` 会返回确定性的 insufficient context,且不调用 generation provider。
247
445
 
248
- Maglev 不绑定特定的策略库。你可以配置一个简单适配器,在检索和回答时应用应用自身的授权规则:
446
+ ## 统一请求与路由
249
447
 
250
- ```ruby
251
- class MaglevAuthorization
252
- def scope(model:, user:)
253
- model.accessible_by(user)
254
- end
448
+ 需要统一 Result envelope 或路线选择时,使用 `Maglev.request`、`Model.maglev_request` 或 `relation.maglev_request`。
255
449
 
256
- def authorize(record:, user:)
257
- record.account_id == user.account_id
258
- end
259
- end
450
+ ```ruby
451
+ result = current_account.invoices.maglev_request(
452
+ "有多少未结发票已经逾期?",
453
+ mode: :structured,
454
+ planner_adapter: planner,
455
+ authorizer: resource_authorizer,
456
+ user: current_user
457
+ )
260
458
 
261
- Maglev.configure do |config|
262
- config.authorization_adapter = MaglevAuthorization.new
263
- end
459
+ result.status
460
+ result.route
461
+ result.kind
462
+ result.value
463
+ result.evidence
464
+ result.warnings
465
+ result.trace_id
466
+ result.confidence
467
+ result.reasons
468
+ result.metadata
264
469
  ```
265
470
 
266
- 适配器必须实现:
471
+ Mode 包括 `:structured`、`:rag`、`:hybrid`、`:auto`。显式 mode 永不调用路由 classifier。自动路由需要 `routing_adapter`,并且只会收到有界 capability summary,不会收到记录值或来源正文。应用级请求必须提供 resources/models 或 base relation;Maglev 永远不会扫描所有应用模型。
267
472
 
268
- - `scope(model:, user:)`:返回该用户可见的记录。
269
- - `authorize(record:, user:)`:返回 `false` 时拒绝访问记录。
473
+ Routing adapter 实现 `classify(question:, capabilities:)`,并返回例如
474
+ `{"route" => "structured", "confidence" => 0.9, "reasons" => ["exact fields"]}`
475
+ 的结果。Confidence 仅供参考,绝不授予权限。
270
476
 
271
- 如果未配置适配器,默认允许访问所有记录。凡是需要限定检索范围的调用,都应一致地传入 `user:`。
477
+ 通过统一 API 获得生成式 RAG 答案时传入 `answer: true`;否则 RAG 路线返回 `kind: :semantic_matches`。
272
478
 
273
- `customer.explain` 是面向无需用户级授权场景的便捷 API;需要用户上下文时,请使用 `customer.ask(Maglev.configuration.explain_question, user: current_user)`。授权作用域适用于默认 pgvector 检索路径。将直接 `search` 调用与自定义存储结合前,请阅读[向量存储](#向量存储)。
479
+ ## 混合流程
274
480
 
275
- ## 配置
481
+ Hybrid 只支持两种固定 shape,并要求一个同时声明 queryable 与 knowledge 的资源。
276
482
 
277
483
  ```ruby
278
- Maglev.configure do |config|
279
- config.embedding_provider do |provider|
280
- provider.url = ENV.fetch("MAGLEV_EMBEDDING_URL", "https://api.openai.com/v1")
281
- provider.api_key = ENV["MAGLEV_EMBEDDING_API_KEY"]
282
- provider.model = "text-embedding-3-small"
283
- provider.dimensions = 1536
284
- end
285
-
286
- config.generation_provider do |provider|
287
- provider.url = ENV.fetch("MAGLEV_GENERATION_URL", "https://api.openai.com/v1")
288
- provider.api_key = ENV["MAGLEV_GENERATION_API_KEY"]
289
- provider.model = "gpt-4.1-mini"
290
- end
291
-
292
- config.chunk_size = 1000
484
+ result = current_account.support_tickets.maglev_request(
485
+ "提到取消问题的未结工单",
486
+ mode: :hybrid,
487
+ hybrid_plan: :structured_first, # 或 :rag_first
488
+ planner_adapter: planner,
489
+ authorizer: resource_authorizer,
490
+ candidate_limit: 100,
491
+ user: current_user
492
+ )
293
493
 
294
- config.context_max_characters = 4000
295
- config.context_per_owner_characters = 1200
296
- config.max_relation_depth = 3
297
-
298
- config.attachment_allowed_content_types = [
299
- "text/plain",
300
- "text/markdown",
301
- "text/html"
302
- ]
303
- config.attachment_max_bytes = 5 * 1024 * 1024
304
- config.attachment_max_characters = 20_000
305
-
306
- config.provider_max_attempts = 2
307
- config.provider_timeout = 30
308
- config.logger = Rails.logger
309
- end
494
+ result.kind # :hybrid_answer
495
+ result.value.records
496
+ result.evidence # structured/RAG provenance
497
+ result.metadata[:plan_shape]
498
+ result.metadata[:operations]
310
499
  ```
311
500
 
312
- `provider_timeout` 对每次 provider 尝试分别生效。超时会被视为可重试错误,并计入 `provider_max_attempts`。
313
-
314
- 当应用需要不同的模型提供商或策略行为时,可注入自定义的 `embedding_adapter`、`generation_adapter`、`attachment_extractor`、`authorization_adapter` 或 `source_redactor`。测试环境可以使用确定性适配器,全程不发起网络请求。
501
+ Structured-first 先筛选记录,再在候选中检索;RAG-first 先检索 owner,再通过授权 relation 验证。候选项只传递有界且经过类型转换的主键;每个阶段都会重新应用注册、租户、base relation 和授权约束。系统不会循环或自主调用工具。
315
502
 
316
- ## 向量存储
503
+ ## 授权与租户
317
504
 
318
- PostgreSQL pgvector 是默认的生产环境方案。对于需要其他存储后端的应用,Maglev 也提供了精简的协议:
505
+ Base relation structured 和 hybrid 的权限边界:
319
506
 
320
507
  ```ruby
321
- class MyVectorStore < Maglev::VectorStores::Base
322
- def upsert(documents:)
323
- # 使用 document.id 持久化或替换文档
324
- end
325
-
326
- def search(vector:, filters:, limit:)
327
- # 返回最近的 Maglev::VectorStores::Document 对象
328
- end
329
-
330
- def delete(ids:)
331
- # 删除具有这些稳定 ID 的文档
332
- end
508
+ current_account.invoices.maglev_request(
509
+ question, mode: :structured, authorizer: resource_authorizer, user: current_user
510
+ )
511
+ policy_scope(Invoice).maglev_request(
512
+ question, mode: :structured, authorizer: resource_authorizer, user: current_user
513
+ ) # Pundit
514
+ Invoice.accessible_by(current_ability).maglev_request(
515
+ question, mode: :structured, authorizer: resource_authorizer, user: current_user
516
+ ) # CanCanCan
517
+ ```
333
518
 
334
- def delete_by_owner(owner_type:, owner_id:)
335
- # 删除属于该所有者的所有文档
336
- end
519
+ RAG 授权使用可选 adapter:
337
520
 
338
- def healthcheck = :ok
339
- def capabilities = {metadata_filtering: true}
521
+ ```ruby
522
+ class MaglevAuthorization
523
+ def scope(model:, user:) = model.where(account_id: user.account_id)
524
+ def authorize(record:, user:) = record.account_id == user.account_id
340
525
  end
341
526
 
342
527
  Maglev.configure do |config|
343
- config.vector_store = MyVectorStore.new
528
+ config.authorization_adapter = MaglevAuthorization.new
529
+ config.tenant_id_resolver = lambda do |record: nil, user: nil|
530
+ (record || user)&.account_id&.to_s
531
+ end
344
532
  end
345
533
  ```
346
534
 
347
- `Maglev::VectorStores::Memory` 适合测试和本地实验。自定义存储应保留文档元数据筛选能力和稳定的文档标识语义。
535
+ 没有 RAG authorization adapter 时,默认允许所有记录。所有用户范围内的检索都应配置 adapter 并传入 `user:`。
348
536
 
349
- 自定义向量存储目前会收到模型和所有者元数据筛选条件,但不会收到已配置的授权作用域。`ask` 仍会逐条授权检索到的所有者;使用自定义存储时,不能将直接 `search(..., user:)` 视为已按授权过滤。需要时请在自定义存储内部应用租户或策略筛选;如果搜索必须遵守授权作用域,请使用默认 pgvector 路径。
537
+ 存储支持时会下推授权过滤;hydrate 后仍会重新检查每条记录。授权 scope 超过 1,000 owner ID 时会 fail closed。
350
538
 
351
- ## 运维与可观测性
539
+ ## 索引、升级与运维
352
540
 
353
541
  ```bash
354
542
  bin/rails maglev:status
355
- bin/rails maglev:reindex[Customer]
543
+ bin/rails maglev:reindex[SupportTicket]
356
544
  bin/rails maglev:reindex_all
545
+ bin/rails maglev:evaluate_planner
357
546
  ```
358
547
 
359
- 重建索引可安全重复执行:未变化的分块会被复用,过期分块会被删除。Maglev 会为索引开始/成功/失败、检索、生成和提供商重试发出 ActiveSupport 通知:
548
+ 相关事务提交后 callback 会入队 `Maglev::ReindexJob`。索引操作幂等、复用未变化 chunk、原子替换单个 owner 的完整可搜索代际,并记录安全的状态和失败诊断。
360
549
 
361
- ```ruby
362
- ActiveSupport::Notifications.subscribe(/\Amaglev\./) do |name, start, finish, id, payload|
363
- Rails.logger.info(
364
- event: name,
365
- duration_ms: ((finish - start) * 1000).round(1),
366
- **payload
367
- )
368
- end
550
+ 从 0.1.x 升级请遵循 [CHANGELOG.md](CHANGELOG.md):
551
+
552
+ ```bash
553
+ bin/rails generate maglev:upgrade_index_version
554
+ bin/rails generate maglev:upgrade_source_identity
555
+ bin/rails db:migrate
556
+ bin/rails maglev:reindex_all
369
557
  ```
370
558
 
371
- ## 安全与能力边界
559
+ 请检查生成的迁移。Embedding 维度变化时需要单独修改 vector 列、重建 HNSW 索引,再完成全量 reindex。旧记录在具有当前 index identity 前不会参与检索。
372
560
 
373
- - 应将提取内容视为不可信上下文。Maglev 会清洗支持的 HTML 来源,但授权和模型公开范围仍由应用负责。
374
- - 只公开适合发送给所配置模型提供商的字段和内容源。
375
- - 默认附件限制为 5 MiB 和 20,000 个提取字符;默认允许纯文本、Markdown、HTML 和 XHTML。
376
- - v1 生成迁移使用 `bigint` 所有者 ID。使用 UUID 的模型需要自定义迁移。
377
- - Maglev 只负责 RAG。它不会生成 SQL、通过数据库计算回答聚合问题、公开 REST 端点、提供管理界面、运行 Agent 或管理对话记忆。
561
+ ### Index identity 与安全替换
378
562
 
379
- ## 兼容性
563
+ 每个 chunk 都保存 `index_version`。Fingerprint 格式版本 1 使用
564
+ `maglev-index` namespace,并覆盖 embedding 模型/维度、adapter ID/版本、
565
+ chunking 算法/大小和 `application_index_version`。自定义 embedding adapter
566
+ 应实现 `maglev_adapter_id` 与 `maglev_adapter_version`,或配置
567
+ `embedding_adapter_id` 与 `embedding_adapter_version`。
380
568
 
381
- CI 测试矩阵如下:
569
+ `upgrade_index_version` 迁移会有意添加可空的 `index_version`。Legacy row
570
+ 在全量 reindex 获得当前 identity 前不可检索。维度变化时必须先迁移 vector
571
+ 列,再执行 reindex。Owner 替换失败时必须保留上一代完整内容。
382
572
 
383
- | | 支持版本 |
384
- |---|---|
385
- | Ruby | 3.2、3.3 |
386
- | Rails | 7.1、8.0 |
387
- | 数据库 | PostgreSQL + pgvector |
573
+ ## Vector store 契约
388
574
 
389
- 矩阵之外的版本可能可以运行,但目前不作保证。
575
+ PostgreSQL/pgvector 是生产默认方案;`Maglev::VectorStores::Memory` 适合测试和本地实验。自定义 store 实现 `fetch(ids:)`、`upsert(documents:)`、`search(vector:, filters:, limit:)`、`delete(ids:)`、`delete_by_owner(owner_type:, owner_id:)`、原子的 `replace_owner(owner_type:, owner_id:, documents:)`、`healthcheck` 和 `capabilities`。
390
576
 
391
- ## 开发
577
+ `delete_by_owner` 后接 `upsert` 不是原子替换。同一 owner 的并发替换/删除必须线性化;替换失败必须保留上一代完整内容。
392
578
 
393
- ```bash
394
- bundle install
579
+ ## Trace、证据与安全边界
395
580
 
396
- # PostgreSQL + pgvector 集成测试
397
- MAGLEV_REQUIRE_POSTGRESQL=true MAGLEV_DATABASE=maglev_test bundle exec rspec
581
+ Maglev Result 包含有界证据和 trace ID。Trace 记录标识符、决策、操作名、限制、安全计时、警告和错误类;默认排除记录值、来源正文、prompt、密钥和原始 provider payload。持久化和保留策略由宿主应用负责。
398
582
 
583
+ Maglev 0.2 **不提供**:
584
+
585
+ - 自然语言写操作或 mutation;
586
+ - 不受限 SQL、Ruby、Arel、scope 或代码执行;
587
+ - 自主/迭代 Agent;
588
+ - Rails Engine、REST API、管理 UI 或强制前端;
589
+ - 内置 PDF/Office/OCR/音频解析;
590
+ - streaming 或对话记忆;
591
+ - Qdrant 或其他强制外部向量服务。
592
+
593
+ 检索文档只是证据,绝不是能够改变路线、权限、Query IR 或执行策略的指令。
594
+
595
+ ## 运行环境支持与开发
596
+
597
+ | 组件 | 支持版本 |
598
+ | --- | --- |
599
+ | Ruby | 3.3、4.0 |
600
+ | Rails | 7.1、8.0 |
601
+ | 数据库 | PostgreSQL + pgvector |
602
+
603
+ ```bash
604
+ bundle exec rspec
399
605
  bundle exec standardrb
400
606
  bundle exec rubocop
401
607
  bundle exec rake build
608
+ bundle exec rake maglev:release_audit
402
609
  ```
403
610
 
404
- 默认测试套件使用确定性的虚拟适配器,不会调用真实的 LLM 或嵌入服务。
611
+ 默认测试使用确定性 fake,不调用线上 LLM 或 embedding provider。
405
612
 
406
613
  ## 许可证
407
614
 
408
- Maglev [MIT License](LICENSE.txt) 开源发布。
615
+ Maglev 使用 [MIT License](LICENSE.txt)