@keo-ai/axiom 0.1.0 → 0.1.2

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.
package/README.md CHANGED
@@ -35,6 +35,8 @@ Axiom 是 **keo** 的 LLM 底座库。它不碰业务编排(意图路由、节
35
35
  │ └──────────┘ ┌─────────────┐ │
36
36
  │ │ RAG基础 │ │
37
37
  │ │ Embedding │ │
38
+ │ │ pgvector │ │
39
+ │ │ rerank │ │
38
40
  │ └─────────────┘ │
39
41
  └────────────────────────────────────────────┘
40
42
  ```
@@ -45,7 +47,7 @@ Axiom 是 **keo** 的 LLM 底座库。它不碰业务编排(意图路由、节
45
47
  - ✅ LLM 统一调用 + Provider 故障转移
46
48
  - ✅ Function Call Loop 的驱动、记录、校验、副作用传播
47
49
  - ✅ 记忆系统的抽象与基础实现
48
- - ✅ RAG 基础(Embedding / 向量检索)
50
+ - ✅ RAG 基础(Embedding / pgvector 向量检索 / rerank)
49
51
  - ✅ 基础配置中心
50
52
 
51
53
  **Axiom 不做:**
@@ -60,6 +62,32 @@ Axiom 是 **keo** 的 LLM 底座库。它不碰业务编排(意图路由、节
60
62
 
61
63
  ---
62
64
 
65
+ ## 安装
66
+
67
+ ```bash
68
+ npm install @keo-ai/axiom pg
69
+ ```
70
+
71
+ > `pg` 是 peer dependency,用于 EmbeddingSearch 模块的 pgvector 连接。
72
+
73
+ 在你的项目根目录创建 `.env` 文件:
74
+
75
+ ```bash
76
+ BAILIAN_API_KEY=your-api-key
77
+ ```
78
+
79
+ 然后在你的项目入口文件顶部引入 `dotenv` 以加载环境变量:
80
+
81
+ ```ts
82
+ import 'dotenv/config';
83
+
84
+ // 或指定路径
85
+ import { config } from 'dotenv';
86
+ config({ path: '.env.local' });
87
+ ```
88
+
89
+ ---
90
+
63
91
  ## Predict 模块(LLM 调用)
64
92
 
65
93
  Predict 是 Axiom 的 LLM 调用层。它封装了 Provider 连接、请求组装、故障转移和返回解析,让上层只需关心「用什么模型、传什么消息」。
@@ -78,7 +106,7 @@ export BAILIAN_API_KEY="your-api-key"
78
106
  import { LLM } from '@keo-ai/axiom';
79
107
 
80
108
  const res = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
81
- console.log(res.content);
109
+ console.log(res);
82
110
  ```
83
111
 
84
112
  ### API 概览
@@ -96,9 +124,8 @@ console.log(res.content);
96
124
 
97
125
  | `responseFormat` | 返回值 | 说明 |
98
126
  |---|---|---|
99
- | `'text'` | `string` | 直接返回模型输出的文本 |
127
+ | 未设置 / `'text'` | `string` | 直接返回模型输出的文本 |
100
128
  | `'json'` | `any` | 自动 `JSON.parse` |
101
- | 未设置 | `LLMResponse` | 完整响应(含 `content`、`usage`、`model`) |
102
129
 
103
130
  ```ts
104
131
  // text → string
@@ -170,6 +197,212 @@ try {
170
197
 
171
198
  ---
172
199
 
200
+ ## EmbeddingSearch 模块(向量检索)
201
+
202
+ EmbeddingSearch 是 Axiom 的 RAG 底座。它封装了 `query → embedding → pgvector 检索 → [可选 rerank]` 的完整链路,只需一行代码即可实现语义检索。
203
+
204
+ ### 需要的列
205
+
206
+ 使用 `EmbeddingSearch` 前,先创建 pgvector 表。**只有 `embedding` 列是必需的**(默认列名 `embedding`,可通过 `embeddingColumn` 改),其余列完全自由:
207
+
208
+ | 列名 | 类型 | 说明 |
209
+ |---|---|---|
210
+ | `embedding` | `vector(1024)` | 向量字段,必需,默认列名 `embedding`,可通过 `embeddingColumn` 自定义 |
211
+ | *(任意)* | *(任意)* | 你自己的业务列,可直接用于过滤和返回 |
212
+
213
+ **开 rerank 时的额外要求**:需指定 `rerankKey` 告诉系统哪一列是文本内容(默认 `'content'`)。
214
+
215
+ ```sql
216
+ CREATE EXTENSION IF NOT EXISTS vector;
217
+
218
+ -- 最简表:不开 rerank,只有向量和业务列
219
+ CREATE TABLE products (
220
+ product_id SERIAL PRIMARY KEY,
221
+ name VARCHAR(100),
222
+ category VARCHAR(50),
223
+ price NUMERIC,
224
+ embedding VECTOR(1024)
225
+ );
226
+
227
+ -- 开 rerank 的表:有文本列供 rerank 使用
228
+ CREATE TABLE documents (
229
+ doc_id SERIAL PRIMARY KEY,
230
+ title VARCHAR(100),
231
+ body TEXT, -- rerankKey: 'body'
232
+ embedding VECTOR(1024)
233
+ );
234
+
235
+ CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops);
236
+ ```
237
+
238
+ ### 快速开始
239
+
240
+ 不传 `select` 时默认返回所有列(`SELECT *`):
241
+
242
+ ```ts
243
+ import { EmbeddingSearch } from '@keo-ai/axiom';
244
+ import { Pool } from 'pg';
245
+
246
+ const pool = new Pool({ connectionString: 'postgresql://...' });
247
+
248
+ // 默认 SELECT *,返回所有列
249
+ const results = await EmbeddingSearch.query('衣服质量怎么样', {
250
+ tableName: 'documents',
251
+ }, pool);
252
+
253
+ for (const r of results) {
254
+ console.log(r.id, r.content, r.embeddingScore, r.rerankScore);
255
+ }
256
+ ```
257
+
258
+ 指定 `select` 只返回需要的列:
259
+
260
+ ```ts
261
+ const results = await EmbeddingSearch.query('衣服质量怎么样', {
262
+ tableName: 'documents',
263
+ select: ['id', 'category', 'price'],
264
+ }, pool);
265
+
266
+ // 只返回 id、category、price 三列,以及 embeddingScore 和 rerankScore
267
+ for (const r of results) {
268
+ console.log(r.category, r.price, r.embeddingScore);
269
+ }
270
+ ```
271
+
272
+ 文本列通过 `rerankKey` 指定:
273
+
274
+ ```ts
275
+ const results = await EmbeddingSearch.query('衣服质量怎么样', {
276
+ tableName: 'documents',
277
+ rerankKey: 'body',
278
+ }, pool);
279
+ ```
280
+
281
+ ### 返回结构
282
+
283
+ `SearchResult` 不固定字段,取决于你查询了哪些列,但始终包含两个 score:
284
+
285
+ ```ts
286
+ interface SearchResult {
287
+ [key: string]: unknown; // 你 select 的列
288
+ embeddingScore: number; // cosine similarity,范围 -1 ~ 1
289
+ rerankScore?: number; // 仅 enableRerank 时存在
290
+ }
291
+ ```
292
+
293
+ ### EmbeddingSearch.query 配置项
294
+
295
+ | 配置项 | 类型 | 默认值 | 说明 |
296
+ |---|---|---|---|
297
+ | `tableName` | `string` | — | 检索表名(必填) |
298
+ | `enableRerank` | `boolean` | `true` | 是否启用 rerank |
299
+ | `embeddingTopK` | `number` | `10` | pgvector 检索返回条数(rerank 的候选集) |
300
+ | `finalTopN` | `number` | `5` | 最终结果条数 |
301
+ | `embeddingThreshold` | `number` | `0.8` | embedding 相似度阈值过滤(cosine similarity) |
302
+ | `rerankThreshold` | `number` | `0.1` | rerank 分数阈值过滤 |
303
+ | `embeddingColumn` | `string` | `'embedding'` | 向量列名 |
304
+ | `rerankKey` | `string` | `'content'` | 启用 rerank 时,用于重排序的文本列名 |
305
+ | `dimensions` | `number` | `1024` | embedding 输出维度(1 ~ 1024) |
306
+ | `select` | `string[]` | `undefined` | 指定返回哪些列,不传则 `SELECT *` |
307
+ | `filter` | `Record<string, unknown>` | — | 对表独立列做等值/范围过滤 |
308
+
309
+ ### 过滤
310
+
311
+ 支持对表的任意独立列做等值和范围过滤:
312
+
313
+ ```ts
314
+ // 等值过滤
315
+ const results = await EmbeddingSearch.query('query', {
316
+ filter: { category: '衣服', shop: '旗舰店' },
317
+ }, pool);
318
+
319
+ // 混合:等值 + 数值范围
320
+ const ranked = await EmbeddingSearch.query('query', {
321
+ filter: {
322
+ category: '衣服',
323
+ price: { $gt: 100, $lt: 500 },
324
+ rating: { $gte: 4 },
325
+ },
326
+ }, pool);
327
+
328
+ // 数组:IN 查询
329
+ const multi = await EmbeddingSearch.query('query', {
330
+ filter: {
331
+ category: ['衣服', '裤子', '鞋子'],
332
+ },
333
+ }, pool);
334
+ ```
335
+
336
+ ### 生成 Embedding
337
+
338
+ 如果你只需要把文本转成向量,直接用 `embed`:
339
+
340
+ ```ts
341
+ import { embed } from '@keo-ai/axiom';
342
+
343
+ const vector = await embed('衣服质量怎么样');
344
+ // vector: number[],默认 1024 维
345
+
346
+ // 指定维度(1 ~ 1024)
347
+ const vector256 = await embed('衣服质量怎么样', 256);
348
+ ```
349
+
350
+ | 参数 | 类型 | 默认值 | 说明 |
351
+ |---|---|---|---|
352
+ | `text` | `string` | — | 要转成向量的文本(必填) |
353
+ | `dimensions` | `number` | `1024` | 输出维度(1 ~ 1024) |
354
+
355
+ ### 纯向量检索(已有向量)
356
+
357
+ `EmbeddingSearch.query` 会自动把文本转成向量并做 rerank。如果你**已经有了向量**(比如自己生成的 embedding),只想做最原始的 pgvector 近邻搜索,用 `EmbeddingSearch.vectorSearch`:
358
+
359
+ ```ts
360
+ import { EmbeddingSearch } from '@keo-ai/axiom';
361
+
362
+ const results = await EmbeddingSearch.vectorSearch({
363
+ pool,
364
+ tableName: 'documents',
365
+ embeddingColumn: 'vec',
366
+ vector: queryVector, // 你自己生成的向量
367
+ topK: 10,
368
+ threshold: 0.7,
369
+ filter: { category: '衣服' },
370
+ });
371
+ ```
372
+
373
+ > `EmbeddingSearch.vectorSearch` **不做 embedding,也不做 rerank**,只负责拿你给的向量去 pgvector 里搜近邻。
374
+
375
+ | 参数 | 类型 | 默认值 | 说明 |
376
+ |---|---|---|---|
377
+ | `pool` | `Pool` | — | pg 连接池(必填) |
378
+ | `tableName` | `string` | — | 检索表名(必填) |
379
+ | `vector` | `number[]` | — | 查询向量(必填) |
380
+ | `topK` | `number` | — | 返回条数(必填) |
381
+ | `threshold` | `number` | — | 相似度阈值 |
382
+ | `embeddingColumn` | `string` | `'embedding'` | 向量列名 |
383
+ | `filter` | `Record<string, unknown>` | — | 对表独立列做过滤 |
384
+ | `select` | `string[]` | `undefined` | 指定返回哪些列 |
385
+
386
+ ### 依赖
387
+
388
+ EmbeddingSearch 模块内部调用百炼的 embedding 和 rerank 服务,无需额外配置:
389
+
390
+ - **Embedding**:`text-embedding-v4`,1024 维,兼容 OpenAI 协议
391
+ - **Rerank**:`qwen3-rerank`,百炼原生 API
392
+
393
+ 只需确保 `BAILIAN_API_KEY` 已设置。
394
+
395
+ ### 错误处理
396
+
397
+ 同 Predict 模块:**直接抛异常**。常见错误:
398
+
399
+ - `BAILIAN_API_KEY environment variable is not set` — 未配置 API Key
400
+ - `[embedding] HTTP 4xx/5xx` — embedding 接口异常
401
+ - `[rerank] HTTP 4xx/5xx` — rerank 接口异常
402
+ - `Invalid table name` — 表名包含非法字符
403
+
404
+ ---
405
+
173
406
  ## 设计理念
174
407
 
175
408
  ```
@@ -0,0 +1 @@
1
+ export declare function embed(text: string, dimensions?: number): Promise<number[]>;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.embed = embed;
13
+ const EMBEDDING_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings';
14
+ const EMBEDDING_MODEL = 'text-embedding-v4';
15
+ function embed(text_1) {
16
+ return __awaiter(this, arguments, void 0, function* (text, dimensions = 1024) {
17
+ var _a;
18
+ const apiKey = process.env.BAILIAN_API_KEY;
19
+ if (!apiKey) {
20
+ throw new Error('BAILIAN_API_KEY environment variable is not set');
21
+ }
22
+ const body = {
23
+ model: EMBEDDING_MODEL,
24
+ input: text,
25
+ dimensions,
26
+ };
27
+ let response;
28
+ try {
29
+ response = yield fetch(EMBEDDING_URL, {
30
+ method: 'POST',
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ Authorization: `Bearer ${apiKey}`,
34
+ },
35
+ body: JSON.stringify(body),
36
+ });
37
+ }
38
+ catch (cause) {
39
+ throw new Error(`[embedding] ${cause instanceof Error ? cause.message : String(cause)}`);
40
+ }
41
+ if (!response.ok) {
42
+ const text = yield response.text();
43
+ throw new Error(`[embedding] HTTP ${response.status}: ${text}`);
44
+ }
45
+ const data = (yield response.json());
46
+ const vector = (_a = data.data[0]) === null || _a === void 0 ? void 0 : _a.embedding;
47
+ if (!vector) {
48
+ throw new Error('[embedding] No embedding in response');
49
+ }
50
+ return vector;
51
+ });
52
+ }
@@ -0,0 +1,27 @@
1
+ import type { Pool } from 'pg';
2
+ import type { EmbeddingSearchConfig, SearchResult } from './types';
3
+ /**
4
+ * 向量检索静态入口。
5
+ *
6
+ * 封装 embedding → pgvector 检索 → [可选 rerank] 的完整链路。
7
+ * PG 连接由外部传入,Axiom 不管理连接池生命周期。
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const results = await EmbeddingSearch.query('query文本', {
12
+ * enableRerank: true,
13
+ * embeddingTopK: 20,
14
+ * finalTopN: 5,
15
+ * embeddingThreshold: 0.7,
16
+ * rerankThreshold: 0.5,
17
+ * }, pgPool);
18
+ * ```
19
+ */
20
+ export declare class EmbeddingSearch {
21
+ static query(query: string, config: EmbeddingSearchConfig, pool: Pool): Promise<SearchResult[]>;
22
+ static vectorSearch(options: Omit<import('./pgvector').PgVectorSearchOptions, 'pool'> & {
23
+ pool: Pool;
24
+ }): Promise<SearchResult[]>;
25
+ }
26
+ export type { EmbeddingSearchConfig, SearchResult } from './types';
27
+ export { embed } from './embed';
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.embed = exports.EmbeddingSearch = void 0;
24
+ const pgvector_1 = require("./pgvector");
25
+ const embed_1 = require("./embed");
26
+ const RERANK_URL = 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank';
27
+ const RERANK_MODEL = 'qwen3-rerank';
28
+ function rerank(query, documents, topN, apiKey) {
29
+ return __awaiter(this, void 0, void 0, function* () {
30
+ const body = {
31
+ model: RERANK_MODEL,
32
+ input: {
33
+ query,
34
+ documents: documents.map((d) => d.content),
35
+ },
36
+ parameters: {
37
+ return_documents: true,
38
+ top_n: topN,
39
+ },
40
+ };
41
+ let response;
42
+ try {
43
+ response = yield fetch(RERANK_URL, {
44
+ method: 'POST',
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ Authorization: `Bearer ${apiKey}`,
48
+ },
49
+ body: JSON.stringify(body),
50
+ });
51
+ }
52
+ catch (cause) {
53
+ throw new Error(`[rerank] ${cause instanceof Error ? cause.message : String(cause)}`);
54
+ }
55
+ if (!response.ok) {
56
+ const text = yield response.text();
57
+ throw new Error(`[rerank] HTTP ${response.status}: ${text}`);
58
+ }
59
+ const data = (yield response.json());
60
+ return data.output.results.map((r) => {
61
+ var _a, _b, _c, _d;
62
+ const doc = documents[r.index];
63
+ return {
64
+ index: r.index,
65
+ id: (_a = doc === null || doc === void 0 ? void 0 : doc.id) !== null && _a !== void 0 ? _a : r.index,
66
+ content: (_d = (_c = (_b = r.document) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : doc === null || doc === void 0 ? void 0 : doc.content) !== null && _d !== void 0 ? _d : '',
67
+ score: r.relevance_score,
68
+ };
69
+ });
70
+ });
71
+ }
72
+ /**
73
+ * 向量检索静态入口。
74
+ *
75
+ * 封装 embedding → pgvector 检索 → [可选 rerank] 的完整链路。
76
+ * PG 连接由外部传入,Axiom 不管理连接池生命周期。
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const results = await EmbeddingSearch.query('query文本', {
81
+ * enableRerank: true,
82
+ * embeddingTopK: 20,
83
+ * finalTopN: 5,
84
+ * embeddingThreshold: 0.7,
85
+ * rerankThreshold: 0.5,
86
+ * }, pgPool);
87
+ * ```
88
+ */
89
+ class EmbeddingSearch {
90
+ static query(query, config, pool) {
91
+ return __awaiter(this, void 0, void 0, function* () {
92
+ const { enableRerank = true, embeddingTopK = 10, finalTopN = 5, embeddingThreshold = 0.8, rerankThreshold = 0.1, tableName, embeddingColumn, rerankKey = 'content', dimensions, filter, select, } = config;
93
+ const idColumn = 'id';
94
+ const actualTopK = enableRerank
95
+ ? Math.max(embeddingTopK, finalTopN)
96
+ : finalTopN;
97
+ const apiKey = process.env.BAILIAN_API_KEY;
98
+ if (!apiKey) {
99
+ throw new Error('BAILIAN_API_KEY environment variable is not set');
100
+ }
101
+ const queryVector = yield (0, embed_1.embed)(query, dimensions);
102
+ // 启用 rerank 时,确保能拿到 rerankKey 对应列的内容
103
+ let actualSelect = select;
104
+ if (enableRerank && actualSelect && !actualSelect.includes(rerankKey)) {
105
+ actualSelect = [...actualSelect, rerankKey];
106
+ }
107
+ const searchResults = yield (0, pgvector_1.vectorSearch)({
108
+ pool,
109
+ tableName,
110
+ vector: queryVector,
111
+ topK: actualTopK,
112
+ threshold: embeddingThreshold,
113
+ embeddingColumn,
114
+ filter,
115
+ select: actualSelect,
116
+ });
117
+ if (searchResults.length === 0) {
118
+ return [];
119
+ }
120
+ if (enableRerank) {
121
+ const missingRerank = searchResults.find((r) => r[rerankKey] === undefined);
122
+ if (missingRerank) {
123
+ throw new Error(`Rerank requires column "${rerankKey}" but it was not found. ` +
124
+ `Either create this column in your table, or set rerankKey to the correct column name.`);
125
+ }
126
+ }
127
+ if (!enableRerank) {
128
+ return searchResults.slice(0, finalTopN);
129
+ }
130
+ const rerankResults = yield rerank(query, searchResults.map((r) => ({
131
+ id: r[idColumn],
132
+ content: r[rerankKey],
133
+ })), finalTopN, apiKey);
134
+ let reranked = [];
135
+ for (const rr of rerankResults) {
136
+ if (rerankThreshold !== undefined && rr.score < rerankThreshold) {
137
+ continue;
138
+ }
139
+ const original = searchResults[rr.index];
140
+ if (original) {
141
+ reranked.push(Object.assign(Object.assign({}, original), { rerankScore: rr.score }));
142
+ }
143
+ }
144
+ // 如果用户没选 rerankKey 对应列,从返回结果中剔除
145
+ if (select && !select.includes(rerankKey)) {
146
+ reranked = reranked.map((r) => {
147
+ const _a = r, _b = rerankKey, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
148
+ return rest;
149
+ });
150
+ }
151
+ return reranked;
152
+ });
153
+ }
154
+ static vectorSearch(options) {
155
+ return __awaiter(this, void 0, void 0, function* () {
156
+ return (0, pgvector_1.vectorSearch)(options);
157
+ });
158
+ }
159
+ }
160
+ exports.EmbeddingSearch = EmbeddingSearch;
161
+ var embed_2 = require("./embed");
162
+ Object.defineProperty(exports, "embed", { enumerable: true, get: function () { return embed_2.embed; } });
@@ -0,0 +1,19 @@
1
+ import type { Pool } from 'pg';
2
+ import type { SearchResult } from './types';
3
+ export interface PgVectorSearchOptions {
4
+ readonly pool: Pool;
5
+ readonly tableName: string;
6
+ readonly vector: number[];
7
+ readonly topK: number;
8
+ readonly threshold?: number;
9
+ readonly embeddingColumn?: string;
10
+ readonly filter?: Record<string, unknown>;
11
+ readonly select?: string[];
12
+ }
13
+ /**
14
+ * 使用 pgvector 执行 cosine similarity 检索。
15
+ *
16
+ * - `embedding <=> query` 为 cosine distance(范围 0~2,越小越相似)
17
+ * - `1 - cosine_distance` 为 cosine similarity(范围 -1~1,越大越相似)
18
+ */
19
+ export declare function vectorSearch(options: PgVectorSearchOptions): Promise<SearchResult[]>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.vectorSearch = vectorSearch;
24
+ function validateIdentifier(name) {
25
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
26
+ throw new Error(`Invalid identifier: ${name}`);
27
+ }
28
+ }
29
+ /**
30
+ * 使用 pgvector 执行 cosine similarity 检索。
31
+ *
32
+ * - `embedding <=> query` 为 cosine distance(范围 0~2,越小越相似)
33
+ * - `1 - cosine_distance` 为 cosine similarity(范围 -1~1,越大越相似)
34
+ */
35
+ function vectorSearch(options) {
36
+ return __awaiter(this, void 0, void 0, function* () {
37
+ const { pool, tableName, vector, topK, threshold, embeddingColumn = 'embedding', filter, select } = options;
38
+ validateIdentifier(tableName);
39
+ validateIdentifier(embeddingColumn);
40
+ const vectorStr = JSON.stringify(vector);
41
+ const params = [vectorStr, topK];
42
+ const conditions = [];
43
+ let paramIndex = 3;
44
+ if (threshold !== undefined) {
45
+ conditions.push(`1 - ("${embeddingColumn}" <=> $1::vector) >= $${paramIndex}`);
46
+ params.push(threshold);
47
+ paramIndex++;
48
+ }
49
+ if (filter && Object.keys(filter).length > 0) {
50
+ for (const [key, value] of Object.entries(filter)) {
51
+ validateIdentifier(key);
52
+ if (Array.isArray(value)) {
53
+ const placeholders = value.map((_, i) => `$${paramIndex + i}`).join(', ');
54
+ conditions.push(`"${key}" IN (${placeholders})`);
55
+ params.push(...value);
56
+ paramIndex += value.length;
57
+ }
58
+ else if (value && typeof value === 'object') {
59
+ const ops = value;
60
+ for (const [op, opValue] of Object.entries(ops)) {
61
+ switch (op) {
62
+ case '$eq':
63
+ conditions.push(`"${key}" = $${paramIndex}`);
64
+ params.push(opValue);
65
+ paramIndex++;
66
+ break;
67
+ case '$gt':
68
+ conditions.push(`"${key}" > $${paramIndex}`);
69
+ params.push(opValue);
70
+ paramIndex++;
71
+ break;
72
+ case '$lt':
73
+ conditions.push(`"${key}" < $${paramIndex}`);
74
+ params.push(opValue);
75
+ paramIndex++;
76
+ break;
77
+ case '$gte':
78
+ conditions.push(`"${key}" >= $${paramIndex}`);
79
+ params.push(opValue);
80
+ paramIndex++;
81
+ break;
82
+ case '$lte':
83
+ conditions.push(`"${key}" <= $${paramIndex}`);
84
+ params.push(opValue);
85
+ paramIndex++;
86
+ break;
87
+ default:
88
+ throw new Error(`Unsupported filter operator: ${op}`);
89
+ }
90
+ }
91
+ }
92
+ else {
93
+ conditions.push(`"${key}" = $${paramIndex}`);
94
+ params.push(value);
95
+ paramIndex++;
96
+ }
97
+ }
98
+ }
99
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
100
+ const selectClause = select && select.length > 0
101
+ ? select.map((c) => { validateIdentifier(c); return `"${c}"`; }).join(', ')
102
+ : '*';
103
+ const sql = `
104
+ SELECT ${selectClause}, 1 - ("${embeddingColumn}" <=> $1::vector) as score
105
+ FROM "${tableName}"
106
+ ${whereClause}
107
+ ORDER BY "${embeddingColumn}" <=> $1::vector
108
+ LIMIT $2
109
+ `;
110
+ const result = yield pool.query(sql, params);
111
+ return result.rows.map((row) => {
112
+ const { score } = row, rest = __rest(row, ["score"]);
113
+ return Object.assign(Object.assign({}, rest), { embeddingScore: score });
114
+ });
115
+ });
116
+ }
@@ -0,0 +1,26 @@
1
+ export interface ComparisonOp {
2
+ readonly $eq?: unknown;
3
+ readonly $gt?: number;
4
+ readonly $lt?: number;
5
+ readonly $gte?: number;
6
+ readonly $lte?: number;
7
+ }
8
+ export type FilterValue = string | number | boolean | null | unknown[] | ComparisonOp;
9
+ export interface EmbeddingSearchConfig {
10
+ readonly enableRerank?: boolean;
11
+ readonly embeddingTopK?: number;
12
+ readonly finalTopN?: number;
13
+ readonly embeddingThreshold?: number;
14
+ readonly rerankThreshold?: number;
15
+ readonly tableName: string;
16
+ readonly embeddingColumn?: string;
17
+ readonly rerankKey?: string;
18
+ readonly dimensions?: number;
19
+ readonly filter?: Record<string, FilterValue>;
20
+ readonly select?: string[];
21
+ }
22
+ export interface SearchResult {
23
+ readonly [key: string]: unknown;
24
+ readonly embeddingScore: number;
25
+ readonly rerankScore?: number;
26
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.d.ts CHANGED
@@ -1,5 +1,34 @@
1
+ /**
2
+ * Axiom — LLM 调用底座库。
3
+ *
4
+ * 使用前需确保环境变量已加载(推荐 dotenv):
5
+ * ```ts
6
+ * import 'dotenv/config';
7
+ * import { LLM } from '@keo-ai/axiom';
8
+ *
9
+ * const res = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
10
+ * ```
11
+ *
12
+ * 必需环境变量:
13
+ * - `BAILIAN_API_KEY` — 百炼 API Key
14
+ *
15
+ * 可选环境变量:
16
+ * - `BAILIAN_BASE_URL` — 自定义百炼 API 地址
17
+ * - `BAILIAN_DEFAULT_MODEL` — 默认模型
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ /** LLM 静态入口类,封装 Provider 连接、故障转移和返回解析 */
1
22
  export { LLM } from './predict';
2
23
  export type { LLMRequest, LLMResponse, Message, ProviderConfig, StreamChunk, } from './predict';
3
24
  export type { LLMProvider, PredictorOptions } from './predict';
4
- export { Predictor, BailianProvider, MODEL_REGISTRY } from './predict';
25
+ /** 预测器核心类,负责模型路由与故障转移 */
26
+ export { Predictor } from './predict';
27
+ /** 百炼 Provider 实现 */
28
+ export { BailianProvider } from './predict';
29
+ /** 模型注册表,定义模型与 Provider 的映射关系 */
30
+ export { MODEL_REGISTRY } from './predict';
5
31
  export type { Model, PredictConfig, PredictWithMessagesConfig } from './predict';
32
+ /** 向量检索(Embedding + pgvector + Rerank) */
33
+ export { EmbeddingSearch, embed } from './embedding_search';
34
+ export type { EmbeddingSearchConfig, SearchResult } from './embedding_search';
package/dist/index.js CHANGED
@@ -1,9 +1,39 @@
1
1
  "use strict";
2
+ /**
3
+ * Axiom — LLM 调用底座库。
4
+ *
5
+ * 使用前需确保环境变量已加载(推荐 dotenv):
6
+ * ```ts
7
+ * import 'dotenv/config';
8
+ * import { LLM } from '@keo-ai/axiom';
9
+ *
10
+ * const res = await LLM.predict({ model: 'qwen3.7-max', prompt: '你好' });
11
+ * ```
12
+ *
13
+ * 必需环境变量:
14
+ * - `BAILIAN_API_KEY` — 百炼 API Key
15
+ *
16
+ * 可选环境变量:
17
+ * - `BAILIAN_BASE_URL` — 自定义百炼 API 地址
18
+ * - `BAILIAN_DEFAULT_MODEL` — 默认模型
19
+ *
20
+ * @packageDocumentation
21
+ */
2
22
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MODEL_REGISTRY = exports.BailianProvider = exports.Predictor = exports.LLM = void 0;
23
+ exports.embed = exports.EmbeddingSearch = exports.MODEL_REGISTRY = exports.BailianProvider = exports.Predictor = exports.LLM = void 0;
24
+ /** LLM 静态入口类,封装 Provider 连接、故障转移和返回解析 */
4
25
  var predict_1 = require("./predict");
5
26
  Object.defineProperty(exports, "LLM", { enumerable: true, get: function () { return predict_1.LLM; } });
27
+ /** 预测器核心类,负责模型路由与故障转移 */
6
28
  var predict_2 = require("./predict");
7
29
  Object.defineProperty(exports, "Predictor", { enumerable: true, get: function () { return predict_2.Predictor; } });
8
- Object.defineProperty(exports, "BailianProvider", { enumerable: true, get: function () { return predict_2.BailianProvider; } });
9
- Object.defineProperty(exports, "MODEL_REGISTRY", { enumerable: true, get: function () { return predict_2.MODEL_REGISTRY; } });
30
+ /** 百炼 Provider 实现 */
31
+ var predict_3 = require("./predict");
32
+ Object.defineProperty(exports, "BailianProvider", { enumerable: true, get: function () { return predict_3.BailianProvider; } });
33
+ /** 模型注册表,定义模型与 Provider 的映射关系 */
34
+ var predict_4 = require("./predict");
35
+ Object.defineProperty(exports, "MODEL_REGISTRY", { enumerable: true, get: function () { return predict_4.MODEL_REGISTRY; } });
36
+ /** 向量检索(Embedding + pgvector + Rerank) */
37
+ var embedding_search_1 = require("./embedding_search");
38
+ Object.defineProperty(exports, "EmbeddingSearch", { enumerable: true, get: function () { return embedding_search_1.EmbeddingSearch; } });
39
+ Object.defineProperty(exports, "embed", { enumerable: true, get: function () { return embedding_search_1.embed; } });
@@ -8,8 +8,6 @@ export interface PredictConfig {
8
8
  readonly model: Model;
9
9
  /** 用户输入的 prompt */
10
10
  readonly prompt: string;
11
- /** 系统提示(可选) */
12
- readonly systemPrompt?: string;
13
11
  readonly temperature?: number;
14
12
  readonly maxTokens?: number;
15
13
  readonly topP?: number;
@@ -21,7 +19,6 @@ export interface PredictConfig {
21
19
  */
22
20
  export interface PredictWithMessagesConfig {
23
21
  readonly model: Model;
24
- readonly systemPrompt?: string;
25
22
  readonly temperature?: number;
26
23
  readonly maxTokens?: number;
27
24
  readonly topP?: number;
@@ -29,9 +26,9 @@ export interface PredictWithMessagesConfig {
29
26
  readonly responseFormat?: 'text' | 'json';
30
27
  }
31
28
  /**
32
- * 将 system prompt 前置到消息列表中。
33
- * @param config - 含可选 systemPrompt 字段的配置
29
+ * 构建消息列表。
30
+ * @param _config - 预测配置(保留以兼容接口,暂不消费)
34
31
  * @param userMessages - 用户提供的消息列表
35
- * @returns 带 system message 前缀的完整列表
32
+ * @returns 完整消息列表
36
33
  */
37
- export declare function buildMessages(config: PredictConfig | PredictWithMessagesConfig, userMessages: ReadonlyArray<Message>): Message[];
34
+ export declare function buildMessages(_config: PredictConfig | PredictWithMessagesConfig, userMessages: ReadonlyArray<Message>): Message[];
@@ -2,16 +2,11 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildMessages = buildMessages;
4
4
  /**
5
- * 将 system prompt 前置到消息列表中。
6
- * @param config - 含可选 systemPrompt 字段的配置
5
+ * 构建消息列表。
6
+ * @param _config - 预测配置(保留以兼容接口,暂不消费)
7
7
  * @param userMessages - 用户提供的消息列表
8
- * @returns 带 system message 前缀的完整列表
8
+ * @returns 完整消息列表
9
9
  */
10
- function buildMessages(config, userMessages) {
11
- const messages = [];
12
- if (config.systemPrompt) {
13
- messages.push({ role: 'system', content: config.systemPrompt });
14
- }
15
- messages.push(...userMessages);
16
- return messages;
10
+ function buildMessages(_config, userMessages) {
11
+ return [...userMessages];
17
12
  }
@@ -1,4 +1,4 @@
1
- import type { Message, LLMResponse, StreamChunk } from './types';
1
+ import type { Message, StreamChunk } from './types';
2
2
  import type { PredictConfig, PredictWithMessagesConfig } from './config';
3
3
  /**
4
4
  * LLM 静态类。封装 Provider 连接、请求组装、故障转移和返回解析。
@@ -9,7 +9,7 @@ import type { PredictConfig, PredictWithMessagesConfig } from './config';
9
9
  * import { LLM } from 'axiom';
10
10
  *
11
11
  * const res = await LLM.predict({ model: 'qwen-max', prompt: '你好' });
12
- * console.log(res.content);
12
+ * console.log(res);
13
13
  * ```
14
14
  */
15
15
  export declare class LLM {
@@ -20,41 +20,33 @@ export declare class LLM {
20
20
  * 内部自动构造单条 user message,支持可选的 system prompt。
21
21
  *
22
22
  * 根据 `responseFormat` 返回不同类型:
23
- * - `'text'` → `string`
23
+ * - 未设置 / `'text'` → `string`
24
24
  * - `'json'` → 解析后的对象(`any`)
25
- * - 未设置 → `LLMResponse`
26
25
  *
27
26
  * @param config - 预测配置(模型、prompt、温度等)
28
27
  * @returns 模型生成的完整响应
29
28
  * @throws Error - Provider 未配置、模型不存在、或所有 Provider 均失败时抛出
30
29
  */
31
- static predict(config: PredictConfig & {
32
- responseFormat: 'text';
33
- }): Promise<string>;
34
30
  static predict(config: PredictConfig & {
35
31
  responseFormat: 'json';
36
32
  }): Promise<any>;
37
- static predict(config: PredictConfig): Promise<LLMResponse>;
33
+ static predict(config: PredictConfig): Promise<string>;
38
34
  /**
39
35
  * 使用自定义消息列表调用 LLM。适用于多轮对话等需要精细控制 message 结构的场景。
40
36
  *
41
37
  * 根据 `responseFormat` 返回不同类型:
42
- * - `'text'` → `string`
38
+ * - 未设置 / `'text'` → `string`
43
39
  * - `'json'` → 解析后的对象(可用泛型指定类型)
44
- * - 未设置 → `LLMResponse`
45
40
  *
46
41
  * @param messages - 消息列表(user/assistant 角色)
47
42
  * @param config - 预测配置(不含 prompt,因为由 messages 提供)
48
43
  * @returns 模型生成的完整响应
49
44
  * @throws Error - Provider 未配置、模型不存在、或所有 Provider 均失败时抛出
50
45
  */
51
- static predictWithMessages(messages: ReadonlyArray<Message>, config: PredictWithMessagesConfig & {
52
- responseFormat: 'text';
53
- }): Promise<string>;
54
46
  static predictWithMessages(messages: ReadonlyArray<Message>, config: PredictWithMessagesConfig & {
55
47
  responseFormat: 'json';
56
48
  }): Promise<any>;
57
- static predictWithMessages(messages: ReadonlyArray<Message>, config: PredictWithMessagesConfig): Promise<LLMResponse>;
49
+ static predictWithMessages(messages: ReadonlyArray<Message>, config: PredictWithMessagesConfig): Promise<string>;
58
50
  /**
59
51
  * 流式调用 LLM,逐块返回模型输出。
60
52
  *
@@ -69,16 +69,14 @@ function toLLMRequest(config, messages) {
69
69
  }
70
70
  function unwrapResponse(response, responseFormat) {
71
71
  var _a;
72
- if (responseFormat === 'text') {
73
- return (_a = response.content) !== null && _a !== void 0 ? _a : '';
74
- }
75
72
  if (responseFormat === 'json') {
76
73
  if (!response.content) {
77
74
  throw new Error('Empty response content when responseFormat is json');
78
75
  }
79
76
  return JSON.parse(response.content);
80
77
  }
81
- return response;
78
+ // 默认按 text 返回(包括未设置 responseFormat 的情况)
79
+ return (_a = response.content) !== null && _a !== void 0 ? _a : '';
82
80
  }
83
81
  /**
84
82
  * LLM 静态类。封装 Provider 连接、请求组装、故障转移和返回解析。
@@ -89,7 +87,7 @@ function unwrapResponse(response, responseFormat) {
89
87
  * import { LLM } from 'axiom';
90
88
  *
91
89
  * const res = await LLM.predict({ model: 'qwen-max', prompt: '你好' });
92
- * console.log(res.content);
90
+ * console.log(res);
93
91
  * ```
94
92
  */
95
93
  class LLM {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keo-ai/axiom",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "基于 LLM 的预测与推理库,支持多 Provider 切换",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "scripts": {
12
12
  "build": "tsc",
13
- "test": "vitest run"
13
+ "test": "vitest run --exclude '**/*.integration.test.ts'"
14
14
  },
15
15
  "dependencies": {
16
16
  "dotenv": "^17.4.2"