@h-ai/ai 0.1.0-alpha8 → 1.0.0-beta.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.
- package/README.en.md +647 -0
- package/README.ja.md +647 -0
- package/README.md +477 -1693
- package/dist/ai-audio-types-4R5yUAVl.d.ts +614 -0
- package/dist/ai-audio-ws-protocol-nfhO0tOo.d.ts +7605 -0
- package/dist/browser.d.ts +8 -5
- package/dist/browser.js +3 -2
- package/dist/chunk-GMQFIWND.js +254 -0
- package/dist/chunk-GMQFIWND.js.map +1 -0
- package/dist/chunk-HEVXR252.js +965 -0
- package/dist/chunk-HEVXR252.js.map +1 -0
- package/dist/chunk-HYMP57AC.js +118 -0
- package/dist/chunk-HYMP57AC.js.map +1 -0
- package/dist/client/index.d.ts +63 -231
- package/dist/client/index.js +2 -1
- package/dist/index.d.ts +48 -15
- package/dist/index.js +6714 -1695
- package/dist/index.js.map +1 -1
- package/package.json +43 -21
- package/dist/ai-reasoning-types-YcmdAlAU.d.ts +0 -2813
- package/dist/ai-types-Dy8dOY4z.d.ts +0 -1095
- package/dist/api/index.d.ts +0 -628
- package/dist/api/index.js +0 -243
- package/dist/api/index.js.map +0 -1
- package/dist/chunk-5P2VX6FO.js +0 -462
- package/dist/chunk-5P2VX6FO.js.map +0 -1
- package/dist/chunk-ABP7RKIP.js +0 -311
- package/dist/chunk-ABP7RKIP.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,1886 +1,670 @@
|
|
|
1
1
|
# @h-ai/ai
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 功能特性
|
|
6
|
-
|
|
7
|
-
| 子系统 | 说明 | 依赖子系统 |
|
|
8
|
-
| ---------- | ------------------------------------------------------------------- | ------------------------------------------------- |
|
|
9
|
-
| Tools | 工具定义(Zod → JSON Schema)、注册表管理、批量执行 | —(纯函数) |
|
|
10
|
-
| Stream | 流式块处理、SSE 编解码、流收集 | —(纯函数) |
|
|
11
|
-
| MCP Server | MCP HTTP 服务器、多传输层支持 | —(独立创建) |
|
|
12
|
-
| Client | 前端轻量客户端(流式响应、SSE 解析、记忆/会话查询) | —(独立创建,依赖 api-client) |
|
|
13
|
-
| LLM | 大模型调用(非流式 / 流式)、简易问答、对话历史 | LLM Provider |
|
|
14
|
-
| Token | Token 估算(文本 / 消息列表) | LLM |
|
|
15
|
-
| MCP | 工具注册与调用、资源读取、提示词管理 | LLM |
|
|
16
|
-
| Embedding | 文本向量化、单条 / 批量嵌入 | LLM Provider |
|
|
17
|
-
| Summary | 消息摘要生成(支持增量摘要) | LLM |
|
|
18
|
-
| Rerank | 重排序(查询-文档相关性排序) | LLM |
|
|
19
|
-
| File | 文件解析(PDF / DOCX / HTML / OCR) | LLM(OCR 需 Vision 模型) |
|
|
20
|
-
| Compress | 上下文压缩(sliding-window / summary / hybrid) | LLM + Summary |
|
|
21
|
-
| Memory | 对话记忆提取、存储、检索、注入(支持 objectId 隔离) | LLM + Embedding + Store |
|
|
22
|
-
| Reasoning | 推理引擎(CoT / ReAct / Plan-Execute) | LLM + Tools |
|
|
23
|
-
| Retrieval | 多来源向量检索、分数过滤 | Embedding + Store |
|
|
24
|
-
| RAG | 检索增强问答(Retrieval + LLM) | LLM + Retrieval |
|
|
25
|
-
| Knowledge | 知识库管理(入库、检索、问答、实体提取) | LLM + Embedding + VecDB + RelDB + Datapipe |
|
|
26
|
-
| Context | 有状态上下文管理器(压缩 + LLM/Memory/RAG/Reasoning 编排 + 持久化) | LLM + Memory + Compress + RAG + Reasoning + Store |
|
|
27
|
-
| A2A | Agent-to-Agent 协议(代理间通信与任务委派) | LLM + A2A 配置(延迟初始化) |
|
|
28
|
-
| Store | 统一存储抽象(对话记录、会话、检索源、记忆、上下文持久化) | AIStoreProvider(默认实现基于 RelDB + VecDB) |
|
|
29
|
-
|
|
30
|
-
## 安装
|
|
3
|
+
[简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
|
|
31
4
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
5
|
+
[框架首页](../../README.md) · 相关模块:[@h-ai/api-contract](../api-contract/README.md) · [@h-ai/core](../core/README.md) · [@h-ai/datapipe](../datapipe/README.md) · [@h-ai/reldb](../reldb/README.md) · [@h-ai/vecdb](../vecdb/README.md)
|
|
6
|
+
|
|
7
|
+
## 简介
|
|
8
|
+
|
|
9
|
+
AI 能力模块,提供统一的 `ai` 服务对象,覆盖 LLM 对话、文生图、工具调用、MCP、Embedding、记忆、检索/RAG、知识库、上下文管理、文件解析、Rerank、语音(ASR/TTS)与 A2A。Node.js 侧通过 `ai.init()` 初始化,浏览器侧通过 API/client 代理访问。
|
|
35
10
|
|
|
36
|
-
##
|
|
11
|
+
## 核心功能清单
|
|
12
|
+
|
|
13
|
+
- `ai.tools`:工具定义、注册表、批量执行;无需初始化。
|
|
14
|
+
- `ai.stream`:ChatCompletion 流式 chunk 处理;无需初始化。
|
|
15
|
+
- `ai.llm`:非流式/流式对话、模型列表、历史记录。
|
|
16
|
+
- `ai.mcp` / `createMcpServer`:内置 MCP 注册表与独立 MCP Server。
|
|
17
|
+
- `ai.embedding`:单条/批量文本向量化。
|
|
18
|
+
- `ai.memory`:记忆提取、存储、召回、注入。
|
|
19
|
+
- `ai.persona`:AI 角色人格档案(系统提示词 + 特征)与系统提示词组合。
|
|
20
|
+
- `ai.retrieval` / `ai.rag`:多源向量检索与检索增强问答。
|
|
21
|
+
- `ai.knowledge`:文档入库、实体增强检索、知识问答。
|
|
22
|
+
- `ai.context`:LLM + Memory + RAG + 压缩的一体化会话管理。
|
|
23
|
+
- `ai.file` / `ai.rerank`:文件解析/OCR 与文本重排序。
|
|
24
|
+
- `ai.audio`:语音识别(ASR)与语音合成(TTS),支持完整与流式调用,覆盖 OpenAI / MiMo / Qwen / 豆包平台。
|
|
25
|
+
- `ai.image`:文生图,覆盖 OpenAI GPT Image、Google Gemini Image / Nano Banana、Qwen-Image 2.0/3.0、Seedream 4.x/5.x 与 Pollinations 免费额度模型。
|
|
26
|
+
- `ai.a2a`:Agent-to-Agent 请求处理与远端调用。
|
|
27
|
+
- `@h-ai/ai/client`:独立浏览器 Audio WebSocket 客户端。
|
|
28
|
+
|
|
29
|
+
`@h-ai/ai/client` 仅提供 `createAudioClient({ url, getTicket })` 与 Audio 客户端类型。HTTP 使用 `@h-ai/api-client` 的 typed 领域方法;默认聊天补全非流式,流与 A2A 使用应用自行绑定的 contract。旧字符串路径客户端和重复 SSE helper 已删除,浏览器不导出 Node 单例 `ai`。
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { apiClient } from '@h-ai/api-client'
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
const ready = await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
35
|
+
if (!ready.success)
|
|
36
|
+
throw new Error(ready.error.message)
|
|
37
|
+
try {
|
|
38
|
+
const response = await apiClient.ai.chats.createCompletion({ messages: [{ role: 'user', content: '你好' }] })
|
|
39
|
+
if (!response.success)
|
|
40
|
+
throw new Error(response.error.message)
|
|
41
|
+
const reply = response.data.choices[0]?.message.content
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await apiClient.close()
|
|
45
|
+
}
|
|
46
|
+
```
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
- `AIStoreProvider`:统一存储抽象;生产要求 `stateScope: 'shared'`,内置实现须先初始化 PostgreSQL/MySQL 与网络 vecdb。开发/显式测试无数据库时使用临时 Store。
|
|
41
49
|
|
|
42
|
-
|
|
43
|
-
- **自定义 Provider**:实现 `AIStoreProvider` 接口后通过 `ai.init(config, { storeProvider })` 注入,无需 reldb / vecdb
|
|
50
|
+
更完整的方法清单、错误码与长示例见 [REFERENCE.md](./REFERENCE.md)。
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
## 使用入门
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
### 快速开始
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
#### 开发 LLM-only(零数据库依赖)
|
|
50
57
|
|
|
51
58
|
```ts
|
|
52
59
|
import { ai } from '@h-ai/ai'
|
|
53
|
-
import { reldb } from '@h-ai/reldb'
|
|
54
|
-
import { vecdb } from '@h-ai/vecdb'
|
|
55
|
-
|
|
56
|
-
// 1. 初始化存储依赖
|
|
57
|
-
await reldb.init({ type: 'sqlite', database: './ai.db' })
|
|
58
|
-
await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
await ai.init({
|
|
61
|
+
const init = await ai.init({
|
|
62
62
|
llm: {
|
|
63
63
|
model: 'gpt-4o-mini',
|
|
64
|
-
apiKey: process.env.
|
|
64
|
+
apiKey: process.env.HAI_AI_LLM_APIKEY,
|
|
65
65
|
},
|
|
66
66
|
})
|
|
67
|
+
if (!init.success)
|
|
68
|
+
return init
|
|
67
69
|
|
|
68
|
-
// 3. 对话
|
|
69
70
|
const result = await ai.llm.chat({
|
|
70
|
-
messages: [
|
|
71
|
-
{ role: 'system', content: '你是一个有帮助的助手' },
|
|
72
|
-
{ role: 'user', content: '你好!' },
|
|
73
|
-
],
|
|
71
|
+
messages: [{ role: 'user', content: '你好!' }],
|
|
74
72
|
})
|
|
75
|
-
if (result.success) {
|
|
76
|
-
const reply = result.data.choices[0].message.content
|
|
77
|
-
}
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
ai.close()
|
|
74
|
+
await ai.close()
|
|
81
75
|
```
|
|
82
76
|
|
|
83
|
-
|
|
77
|
+
开发/显式测试未初始化 reldb/vecdb 时,AI 使用进程内临时 Store;关闭、退出或切换实例后不保留。`NODE_ENV` 或 `HAI_CORE_ENV` 为 production 时,临时 Store、SQLite 或本地向量后端会使初始化失败。生产必须先初始化共享 RelDB 与网络 vecdb,或传入满足同等原子契约的共享 Provider。
|
|
78
|
+
|
|
79
|
+
#### 持久化 DB Provider(reldb + vecdb)
|
|
84
80
|
|
|
85
81
|
```ts
|
|
86
|
-
import type { AIStoreProvider } from '@h-ai/ai'
|
|
87
82
|
import { ai } from '@h-ai/ai'
|
|
83
|
+
import { reldb } from '@h-ai/reldb'
|
|
84
|
+
import { vecdb } from '@h-ai/vecdb'
|
|
88
85
|
|
|
89
|
-
//
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
await
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
86
|
+
// 开发/测试使用本地后端;生产改为共享 PostgreSQL/MySQL 与网络 vecdb。
|
|
87
|
+
const failures: unknown[] = []
|
|
88
|
+
try {
|
|
89
|
+
const database = await reldb.init({ type: 'sqlite', database: './ai.db' })
|
|
90
|
+
if (!database.success)
|
|
91
|
+
throw new Error(database.error.message)
|
|
92
|
+
const vectors = await vecdb.init({ type: 'lancedb', path: './ai-vec.db' })
|
|
93
|
+
if (!vectors.success)
|
|
94
|
+
throw new Error(vectors.error.message)
|
|
95
|
+
const initialized = await ai.init({
|
|
96
|
+
llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY },
|
|
97
|
+
})
|
|
98
|
+
if (!initialized.success)
|
|
99
|
+
throw new Error(initialized.error.message)
|
|
100
|
+
const result = await ai.llm.chat({ messages: [{ role: 'user', content: 'Hello!' }] })
|
|
101
|
+
if (!result.success)
|
|
102
|
+
throw new Error(result.error.message)
|
|
103
|
+
const reply = result.data.choices[0]?.message.content ?? ''
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
failures.push(error)
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await ai.close()
|
|
110
|
+
for (const service of [vecdb, reldb]) {
|
|
111
|
+
const closed = await service.close()
|
|
112
|
+
if (!closed.success)
|
|
113
|
+
failures.push(new Error(closed.error.message))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (failures.length)
|
|
117
|
+
throw new AggregateError(failures, 'AI operation or cleanup failed')
|
|
101
118
|
```
|
|
102
119
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
## 初始化与配置
|
|
120
|
+
MySQL 首次初始化允许多个节点并发建索引:仅接受 mysql2 明确的 ER_DUP_KEYNAME/1061,并重新核对列及顺序、非唯一完整列、升序可见 BTREE 和无表达式;已有同名错误结构仍失败。账户需要建表和 INDEX 权限,元数据读取、权限或其他 DDL 错误使 ai.init 返回 CONFIGURATION_ERROR。返回前等待全部已启动 DDL 结束;MySQL DDL 不回滚,失败保留已创建结构,修复权限/结构后再重试。逐步检查初始化结果,失败不接收业务请求,并逆序关闭 AI、vecdb、reldb。
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
await ai.init({
|
|
111
|
-
// LLM 配置
|
|
112
|
-
llm: {
|
|
113
|
-
apiKey: 'sk-xxx',
|
|
114
|
-
baseUrl: 'https://api.openai.com/v1',
|
|
115
|
-
model: 'gpt-4o-mini', // 默认模型
|
|
116
|
-
maxTokens: 4096,
|
|
117
|
-
temperature: 0.7,
|
|
118
|
-
timeout: 60000, // 请求超时(毫秒)
|
|
119
|
-
// 多模型注册
|
|
120
|
-
models: [
|
|
121
|
-
{ id: 'fast', model: 'gpt-4o-mini', temperature: 0.3 },
|
|
122
|
-
{ id: 'strong', model: 'gpt-4o', maxTokens: 8192 },
|
|
123
|
-
{ id: 'embed', model: 'text-embedding-3-small', baseUrl: 'https://...' },
|
|
124
|
-
],
|
|
125
|
-
scenarios: {
|
|
126
|
-
chat: 'fast',
|
|
127
|
-
reasoning: 'strong',
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
// Embedding 配置
|
|
132
|
-
embedding: {
|
|
133
|
-
dimensions: 1536,
|
|
134
|
-
batchSize: 100,
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
// Knowledge 配置
|
|
138
|
-
knowledge: {
|
|
139
|
-
collection: 'hai_ai_knowledge',
|
|
140
|
-
dimension: 1536,
|
|
141
|
-
enableEntityExtraction: true,
|
|
142
|
-
cleanOptions: {
|
|
143
|
-
removeHtml: true,
|
|
144
|
-
normalizeWhitespace: true,
|
|
145
|
-
},
|
|
146
|
-
chunkOptions: {
|
|
147
|
-
mode: 'markdown', // sentence | paragraph | markdown | page | custom
|
|
148
|
-
maxSize: 1500,
|
|
149
|
-
overlap: 200,
|
|
150
|
-
},
|
|
151
|
-
entityBoostWeight: 0.15, // 实体命中加权系数 [0, 1]
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
// Memory 配置
|
|
155
|
-
memory: {
|
|
156
|
-
maxEntries: 1000,
|
|
157
|
-
embeddingEnabled: true, // 启用向量检索(关闭则仅关键词匹配)
|
|
158
|
-
recencyDecay: 0.95, // 时间衰减系数
|
|
159
|
-
defaultTopK: 10,
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
// Token 配置
|
|
163
|
-
token: {
|
|
164
|
-
tokenRatio: 0.25, // Token 估算系数(4 字符 ≈ 1 token)
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
// Summary 配置
|
|
168
|
-
summary: {
|
|
169
|
-
systemPrompt: '...', // 可选,覆盖内置摘要提示词
|
|
170
|
-
},
|
|
171
|
-
|
|
172
|
-
// Compress 配置
|
|
173
|
-
compress: {
|
|
174
|
-
defaultStrategy: 'hybrid', // summary | sliding-window | hybrid
|
|
175
|
-
defaultMaxTokens: 0, // 0 = 模型 maxTokens × 80%
|
|
176
|
-
preserveLastN: 4, // 保留最近 N 条不压缩
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
// MCP 配置
|
|
180
|
-
mcp: {
|
|
181
|
-
server: {
|
|
182
|
-
name: 'my-app',
|
|
183
|
-
version: '1.0.0',
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
})
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### 配合 core 配置文件使用
|
|
122
|
+
#### 自定义 StoreProvider
|
|
190
123
|
|
|
191
124
|
```ts
|
|
125
|
+
import type { AIStoreProvider } from '@h-ai/ai'
|
|
192
126
|
import { ai } from '@h-ai/ai'
|
|
193
|
-
import { core } from '@h-ai/core'
|
|
194
127
|
|
|
195
|
-
|
|
196
|
-
await ai.init(core.config.get('ai'))
|
|
197
|
-
// ...
|
|
198
|
-
ai.close()
|
|
199
|
-
await core.close()
|
|
200
|
-
```
|
|
128
|
+
const storeProvider: AIStoreProvider = createMyStoreProvider()
|
|
201
129
|
|
|
202
|
-
|
|
130
|
+
await ai.init(
|
|
131
|
+
{ llm: { model: 'gpt-4o-mini', apiKey: process.env.HAI_AI_LLM_APIKEY } },
|
|
132
|
+
{ storeProvider },
|
|
133
|
+
)
|
|
203
134
|
|
|
204
|
-
|
|
205
|
-
ai.isInitialized // boolean
|
|
206
|
-
ai.config // 当前配置对象,未初始化时为 null
|
|
135
|
+
await ai.close()
|
|
207
136
|
```
|
|
208
137
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
## LLM 对话 — `ai.llm`
|
|
212
|
-
|
|
213
|
-
### 非流式对话
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
const result = await ai.llm.chat({
|
|
217
|
-
messages: [
|
|
218
|
-
{ role: 'system', content: '你是一个翻译助手' },
|
|
219
|
-
{ role: 'user', content: '将以下内容翻译为英文:你好世界' },
|
|
220
|
-
],
|
|
221
|
-
temperature: 0.3,
|
|
222
|
-
max_tokens: 1000,
|
|
223
|
-
})
|
|
138
|
+
### API 契约
|
|
224
139
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
```
|
|
140
|
+
- 对外只通过 `ai` 服务对象和少量独立工厂(如 `createMcpServer`)访问。
|
|
141
|
+
- 生命周期为 `await ai.init(config, options?)` / `await ai.close()`;关闭会等待自定义 `AIStoreProvider.close()`。
|
|
142
|
+
- 领域方法返回 `HaiResult<T>` 或 `Promise<HaiResult<T>>`;业务失败通过 `result.success === false` 和 `result.error.code` 表达。流式 `AsyncIterable`、客户端传输和第三方回调在建立或迭代期间可能抛异常。
|
|
143
|
+
- `ai.tools` 与 `ai.stream` 是纯函数子系统,无需初始化即可使用。
|
|
144
|
+
- 开发/测试缺少 reldb/vecdb 时使用临时 Store;两者均已初始化时自动使用 DB Provider。生产校验真实数据库与向量后端共享能力,自定义 Provider 必须如实声明 `stateScope`。
|
|
231
145
|
|
|
232
|
-
|
|
146
|
+
公共扩展通过 AIStoreProvider 注入 Store;内置 LLM Provider/工厂依赖与 Context 内部依赖不属于公开 API。模型接入使用配置,历史查询、AskOptions 和 ChatRecord 保持公开。
|
|
233
147
|
|
|
234
|
-
|
|
235
|
-
// 使用 init 时注册的模型 ID 或直接传模型名
|
|
236
|
-
const result = await ai.llm.chat({
|
|
237
|
-
model: 'gpt-4o',
|
|
238
|
-
messages: [{ role: 'user', content: '复杂推理问题' }],
|
|
239
|
-
})
|
|
240
|
-
```
|
|
148
|
+
### API 概览
|
|
241
149
|
|
|
242
|
-
|
|
150
|
+
#### LLM
|
|
243
151
|
|
|
244
152
|
```ts
|
|
245
|
-
const
|
|
246
|
-
|
|
153
|
+
const result = await ai.llm.chat({ messages })
|
|
247
154
|
for await (const chunk of ai.llm.chatStream({ messages })) {
|
|
248
155
|
const delta = chunk.choices[0]?.delta?.content
|
|
249
156
|
if (delta) {
|
|
250
|
-
process.stdout.write(delta)
|
|
157
|
+
process.stdout.write(delta)
|
|
251
158
|
}
|
|
252
159
|
}
|
|
253
|
-
```
|
|
254
160
|
|
|
255
|
-
|
|
161
|
+
// 请求取消:传入 AbortSignal,主持人打断/用户切换时 abort() 立即停止上游生成与计费
|
|
162
|
+
const controller = new AbortController()
|
|
163
|
+
const cancellable = ai.llm.chat({ messages, signal: controller.signal })
|
|
164
|
+
// controller.abort()
|
|
256
165
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
166
|
+
// 多协议:模型的 api 决定底层走 Chat Completions / Responses / Anthropic,公共请求响应形状不变
|
|
167
|
+
// - chat(默认):OpenAI Chat Completions(兼容绝大多数厂商)
|
|
168
|
+
// - responses:OpenAI Responses API(/v1/responses)
|
|
169
|
+
// - anthropic:Anthropic Messages API(Claude 原生协议,环境变量 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL)
|
|
170
|
+
const claude = await ai.llm.chat({ messages, model: 'claude' }) // 该模型配置 api: anthropic
|
|
171
|
+
|
|
172
|
+
// 临时模型:单次请求绕过配置注册模型,直接指定端点与凭据(chat/chatStream/ask/askStream 均支持)
|
|
173
|
+
// 临时客户端按 TTL 缓存(llm.tempModelCacheTtl,默认 10 分钟),与常驻模型客户端隔离
|
|
174
|
+
const temp = await ai.llm.chat({
|
|
175
|
+
messages,
|
|
176
|
+
tempModel: { model: 'claude-3-5-sonnet', api: 'anthropic', apiKey: 'sk-temp' },
|
|
267
177
|
})
|
|
268
178
|
```
|
|
269
179
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
```ts
|
|
273
|
-
const models = await ai.llm.listModels()
|
|
274
|
-
if (models.success) {
|
|
275
|
-
// models.data: string[]
|
|
276
|
-
}
|
|
277
|
-
```
|
|
180
|
+
SSE 文本解码遵循 [WHATWG 事件流规则](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation):LF/CRLF/CR 跨片、多行 data、仅去冒号后一个空格、继承有效 id/retry;空或缺省 event 按 message,EOF 不补空行派发残余事件。encodeSSE 规范化 data 换行,并对 id/event 换行、id 的 NUL 或无效 retry 显式抛错。Anthropic/MiMo 共用内部 UTF-8 流读取;消费方退出、取消、协议结束和失败均取消正文并释放 reader,不自动重连或重放业务。Anthropic timeout 覆盖正文,MiMo 保留每个 HTTP 请求的独立 timeout。
|
|
278
181
|
|
|
279
|
-
|
|
182
|
+
#### 工具调用
|
|
280
183
|
|
|
281
184
|
```ts
|
|
282
|
-
|
|
283
|
-
const result = await ai.llm.ask('什么是 TypeScript?')
|
|
284
|
-
if (result.success) {
|
|
285
|
-
console.warn(result.data) // string
|
|
286
|
-
}
|
|
185
|
+
import { z } from 'zod'
|
|
287
186
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
187
|
+
const registry = ai.tools.createRegistry({
|
|
188
|
+
// 统一授权在 Zod 校验后、handler 前执行;false/异常均 fail-closed
|
|
189
|
+
authorize: ({ toolName, context }) => canExecuteTool(context.objectId, toolName),
|
|
190
|
+
})
|
|
191
|
+
const registered = registry.register(ai.tools.define({
|
|
192
|
+
name: 'get_weather',
|
|
193
|
+
description: '获取天气',
|
|
194
|
+
parameters: z.object({ city: z.string() }),
|
|
195
|
+
// handler 第二参为执行上下文:可响应取消(打断/超时)、感知截止时间与交互主体
|
|
196
|
+
handler: async ({ city }, { signal }) => {
|
|
197
|
+
const res = await fetch(`https://api.example.com/weather?city=${city}`, { signal })
|
|
198
|
+
return res.json()
|
|
199
|
+
},
|
|
200
|
+
timeoutMs: 10_000, // 本工具默认超时(可被 execute 的 deadline / timeoutMs 覆盖)
|
|
201
|
+
}))
|
|
202
|
+
if (!registered.success)
|
|
203
|
+
return registered
|
|
293
204
|
|
|
294
|
-
|
|
205
|
+
// 执行时可传入取消信号 / 超时 / 作用域;取消或超时返回 TOOL_TIMEOUT,且不再等待未响应的 handler
|
|
206
|
+
const result = await registry.execute(toolCall, { signal: controller.signal, objectId: 'user-001', timeoutMs: 30_000 })
|
|
295
207
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const history = await ai.llm.getHistory('user-001', { sessionId: 'sess-001', limit: 50 })
|
|
299
|
-
if (history.success) {
|
|
300
|
-
// history.data: ChatRecord[]
|
|
301
|
-
}
|
|
208
|
+
// 批量调用默认串行,避免副作用工具并发启动;纯读取且确认安全时才显式并行
|
|
209
|
+
const batch = await registry.executeAll(toolCalls)
|
|
302
210
|
|
|
303
|
-
|
|
304
|
-
const sessions = await ai.llm.listSessions('user-001')
|
|
305
|
-
if (sessions.success) {
|
|
306
|
-
// sessions.data: SessionInfo[]
|
|
307
|
-
}
|
|
211
|
+
const chat = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
|
|
308
212
|
```
|
|
309
213
|
|
|
310
|
-
|
|
214
|
+
#### A2A 协议边界
|
|
311
215
|
|
|
312
|
-
|
|
216
|
+
`ai.a2a.getAgentCard()` 返回完整 A2A 0.3 Agent Card,含 `protocolVersion`、MIME 和 streaming 能力;Kit 默认发现路径为 `/.well-known/agent-card.json`。`handleRequest()` 始终返回 JSON-RPC/SSE 处理结果,未初始化返回 JSON-RPC 错误。可信 HTTP 认证上下文的 `agentId` 映射为 executor 的 `context.user.userName`。当前支持 JSON-RPC/SSE,不声明 push notifications、gRPC 或 REST 支持;业务资源授权仍由应用负责。
|
|
313
217
|
|
|
314
|
-
|
|
218
|
+
A2A 的 journal、context 归属与消息表在 `ai.init()` 阶段初始化。任务按可信 `agentId` 隔离(含获取、取消和续接);匿名请求共享匿名主体,生产应在可信传输层认证。`listMessages` 是服务端管理接口,支持 `callerId`、`contextId`、`since`,不能向未授权客户端暴露。
|
|
315
219
|
|
|
316
|
-
|
|
220
|
+
Task 与事件使用同一共享 journal,不再装配独立的旧 TaskStore。push notification 配置和 authenticated extended card RPC 返回 SDK 标准“不支持/未配置”错误,不执行任务;executor 相关类型保留公开导出,内部 handler/Store 装配类型不属于公共契约。
|
|
317
221
|
|
|
318
|
-
|
|
319
|
-
import { ai } from '@h-ai/ai'
|
|
320
|
-
import { z } from 'zod'
|
|
222
|
+
入站消息必须持久化后才进入 executor,并在等待后重新校验共享执行代际与期限;agent 消息在同一有界事件队列内等待日志成功后才提交 journal。日志失败会产生 failed/interrupted Task,不会发布虚假的成功结果;同一消息重试不重新执行。日志与 journal 不是跨记录事务,已有日志不等于成功交付,最终以 Task 的 `requiresEffectReconciliation` 标记核对可能已发生的业务效果。消息查询失败返回 `A2A_LIST_MESSAGES_FAILED`(984),不暴露原始 SQL 或连接详情。
|
|
321
223
|
|
|
322
|
-
|
|
323
|
-
name: 'get_weather',
|
|
324
|
-
description: '获取指定城市的天气信息',
|
|
325
|
-
parameters: z.object({
|
|
326
|
-
city: z.string().describe('城市名称'),
|
|
327
|
-
unit: z.enum(['celsius', 'fahrenheit']).optional().describe('温度单位'),
|
|
328
|
-
}),
|
|
329
|
-
handler: async ({ city, unit }) => {
|
|
330
|
-
// 调用天气 API
|
|
331
|
-
return { temperature: 20, city, unit: unit ?? 'celsius' }
|
|
332
|
-
},
|
|
333
|
-
})
|
|
334
|
-
```
|
|
224
|
+
Task 与事件一次 CAS 持久化,任意节点可订阅;相同 message ID/内容重试仅返回同一轮结果,改变内容或重复旧轮消息失败。跨节点取消由真实执行 owner 调用 executor.cancelTask,取消未确认期间同一 Task 不接受新消息,避免迟到取消作用于新执行;input-required 可在节点重启后续接。DB 租约过期、绝对期限到达或执行器只返回 working 时标记 failed/interrupted,并设置 `metadata.haiExecution.requiresEffectReconciliation=true`;旧节点恢复不能写入,不自动重放未知外部效果。`a2a.execution` 默认 `leaseMs: 30000`、`pollIntervalMs: 250`、`maxExecutionMs: 300000`。单消息 1 MiB、journal 8 MiB、单轮 2048 事件、Task 256 回执、32 个 reference tasks,超限明确失败。外部副作用须使用稳定业务幂等键并核对未知结果;详细部署/验证边界见[共享状态能力表](../../.agents/instructions/distributed-state.md)。
|
|
335
225
|
|
|
336
|
-
|
|
226
|
+
#### MCP Server
|
|
227
|
+
|
|
228
|
+
`ai.mcp` 是进程内注册表,独立 `createMcpServer` 使用 SDK transport,两者不会自动互相注册。`ai.mcp` 在注册时编译工具 JSON Schema,调用时校验输入;资源输出必须恰好含 `text` / `blob` 之一;提示词参数与 SDK 消息输出均校验,支持图片和音频内容。HTTP 示例只展示 transport 连接,应用仍须挂载请求处理与认证,并在退出时关闭 server。
|
|
337
229
|
|
|
338
230
|
```ts
|
|
339
|
-
|
|
231
|
+
import { createMcpServer, StreamableHTTPServerTransport } from '@h-ai/ai'
|
|
232
|
+
import { z } from 'zod'
|
|
340
233
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
234
|
+
const server = createMcpServer({ name: 'my-server', version: '1.0.0' })
|
|
235
|
+
server.registerTool('search', {
|
|
236
|
+
description: '搜索',
|
|
237
|
+
inputSchema: { query: z.string() },
|
|
238
|
+
}, async ({ query }) => ({
|
|
239
|
+
content: [{ type: 'text', text: `Results for ${query}` }],
|
|
240
|
+
}))
|
|
241
|
+
await server.connect(new StreamableHTTPServerTransport({ sessionIdGenerator: () => globalThis.crypto.randomUUID() }))
|
|
242
|
+
```
|
|
344
243
|
|
|
345
|
-
|
|
346
|
-
registry.has('get_weather') // true
|
|
347
|
-
registry.get('get_weather') // Tool 对象
|
|
348
|
-
registry.getNames() // ['get_weather', ...]
|
|
349
|
-
registry.size // 注册数量
|
|
244
|
+
#### Memory / RAG / Knowledge
|
|
350
245
|
|
|
351
|
-
|
|
352
|
-
const definitions = registry.getDefinitions() // ToolDefinition[]
|
|
246
|
+
Knowledge 每次导入生成唯一 `revision` 和分块 ID,先完整准备分块、向量及实体,再持久化暂存清单、写入向量,最后通过共享 Store CAS 一次发布正文、实体和元数据。相同文档并发重建只允许一个写入者;失败时保留完整旧版,缩短文档不会留下可见旧尾块。检索只接受当前发布版本,并在发布穿插时重试,持续冲突返回错误供调用方重试。向量候选可能按需扩大,清理积压会增加查询成本。
|
|
353
247
|
|
|
354
|
-
|
|
355
|
-
const toolMessage = await registry.execute(toolCall) // HaiResult<ToolMessage>
|
|
248
|
+
`ingest` 成功结果包含 `revision` 与 `cleanupPending`:后者为 true 表示新版本已经发布,但旧向量删除仍待补偿。调用 `repair({ collection, offset, limit })` 分页重试;`setup` 自动检查前 100 份清单,生产环境应定期扫描全部页。暂存期限为存储端时钟的 5 分钟,超时写入者不能发布;故障暂存的永久墓碑用于反复删除迟到的外部写入,不能随日志保留期删除。SQL 与 vecdb 不组成跨库事务,恢复依赖持久清单和持续补偿。`removeDocument` / `deleteCollection` 先撤销可见性,清理失败返回错误;集合删除保留空向量集合及代际墓碑以支持补偿,重新 setup 不显示上一代内容。
|
|
356
249
|
|
|
357
|
-
|
|
358
|
-
const toolMessages = await registry.executeAll(toolCalls, { parallel: true })
|
|
250
|
+
默认 DB Store 传播集合查询/创建及向量写入失败。LanceDB 使用持久主键原子 merge;Qdrant 写入/删除等待应用完成,计数使用精确计数。多节点必须共享关系存储和服务端向量后端;自定义 `KnowledgeStore` 须实现相同的版本发布与补偿契约。
|
|
359
251
|
|
|
360
|
-
|
|
361
|
-
registry.unregister('get_weather')
|
|
362
|
-
registry.clear()
|
|
363
|
-
```
|
|
252
|
+
使用 LanceDB 时,集合维度与 metric 随 Arrow schema 持久化;连接 metric 只影响新集合。重启后复用同一持久目录,embedding 维数改变时重建匹配集合;多个节点各自的本地路径不会自动共享,分布式 Knowledge 推荐服务端向量后端。
|
|
364
253
|
|
|
365
|
-
|
|
254
|
+
Knowledge 的 `retrieve/ask` 及默认 DB Store 会将 `filter` 传给向量 Provider,过滤校验/查询失败会向上传播;Knowledge 返回 `KNOWLEDGE_RETRIEVE_FAILED`(ask 包装为 `RAG_FAILED`),不会伪装成空结果。LanceDB 的过滤只支持顶层键的 string/有限 number/boolean/null AND 精确匹配,例如 `{ tenant: 'acme', active: true }`;null 不匹配缺失字段,不支持对象/数组/undefined。条件在向量 topK 前执行;其它后端的 null/复杂值语义须单独核对。LanceDB 元数据改用原生 JSON 字段,仍可保存嵌套 JSON;旧字符串元数据集合须重建后才能写入或过滤。
|
|
366
255
|
|
|
367
256
|
```ts
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
tool_choice: 'auto', // 'auto' | 'none' | { type: 'function', function: { name: '...' } }
|
|
372
|
-
})
|
|
373
|
-
|
|
374
|
-
if (result.success) {
|
|
375
|
-
const choice = result.data.choices[0]
|
|
376
|
-
|
|
377
|
-
// 检查是否有工具调用
|
|
378
|
-
if (choice.finish_reason === 'tool_calls' && choice.message.tool_calls) {
|
|
379
|
-
// 执行所有工具调用并获得 ToolMessage[]
|
|
380
|
-
const toolResults = await registry.executeAll(choice.message.tool_calls, { parallel: true })
|
|
381
|
-
if (toolResults.success) {
|
|
382
|
-
// 将工具结果追加到消息继续对话
|
|
383
|
-
messages.push(choice.message) // assistant 消息(含 tool_calls)
|
|
384
|
-
messages.push(...toolResults.data) // tool 结果消息
|
|
385
|
-
}
|
|
386
|
-
}
|
|
257
|
+
const enriched = await ai.memory.injectMemories(messages, { objectId: 'user-001', topK: 5 })
|
|
258
|
+
if (!enriched.success) {
|
|
259
|
+
return enriched
|
|
387
260
|
}
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
---
|
|
391
|
-
|
|
392
|
-
## 流处理 — `ai.stream`
|
|
393
261
|
|
|
394
|
-
|
|
262
|
+
// 多租户推荐入口:scoped() 绑定主体与作用域,所有操作自动携带 objectId / scope(含归属校验),杜绝「忘记传 objectId」越权
|
|
263
|
+
const memory = ai.memory.scoped({ objectId: 'user-001', scope: { topicId: 't-1', personaId: 'p-1' } })
|
|
264
|
+
await memory.add({ content: '用户偏好中文', type: 'preference' })
|
|
265
|
+
const recalled = await memory.recall('语言偏好')
|
|
395
266
|
|
|
396
|
-
|
|
267
|
+
// clear 拒绝空过滤(防误清全局);全局清空只能显式走管理接口
|
|
268
|
+
await memory.clear({ types: ['event'] }) // 仅清该作用域内的 event
|
|
269
|
+
await ai.memory.admin.clearAll({ confirm: true }) // 危险:清空整个记忆后端,需显式确认
|
|
397
270
|
|
|
398
|
-
|
|
399
|
-
const processor = ai.stream.createProcessor()
|
|
271
|
+
const rag = await ai.rag.query('核心架构是什么?', { sources: ['docs'], topK: 5 })
|
|
400
272
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
273
|
+
const setup = await ai.knowledge.setup()
|
|
274
|
+
if (setup.success) {
|
|
275
|
+
const ingested = await ai.knowledge.ingest({ documentId: 'doc-001', content: markdownText, title: '产品手册' })
|
|
276
|
+
if (!ingested.success)
|
|
277
|
+
return ingested
|
|
278
|
+
if (ingested.data.cleanupPending) {
|
|
279
|
+
const repaired = await ai.knowledge.repair({ offset: 0, limit: 100 })
|
|
280
|
+
if (!repaired.success)
|
|
281
|
+
return repaired
|
|
405
282
|
}
|
|
406
283
|
}
|
|
407
|
-
|
|
408
|
-
// 获取完整结果
|
|
409
|
-
const result = processor.getResult()
|
|
410
|
-
// { content: string, toolCalls: ToolCall[], finishReason: string | null }
|
|
411
|
-
|
|
412
|
-
// 转为 assistant 消息(可直接 push 到 messages 数组)
|
|
413
|
-
const assistantMsg = processor.toAssistantMessage()
|
|
414
|
-
// { role: 'assistant', content: '...', tool_calls?: [...] }
|
|
415
|
-
|
|
416
|
-
// 重置(复用 processor)
|
|
417
|
-
processor.reset()
|
|
418
284
|
```
|
|
419
285
|
|
|
420
|
-
|
|
286
|
+
Knowledge 分块直接遵循 datapipe 的单位与硬上限:word 为词,其他为 Unicode code point,保留清洗后文本的分隔符;AI 默认 chunkOptions 仍为 markdown/maxSize=1500/overlap=200,覆盖较小 maxSize 时须同时保证继承的 overlap 小于它。分块配置/表达式失败返回 KNOWLEDGE_INGEST_FAILED,embedding 与版本发布均不会执行,不退回整篇文本绕过预算。
|
|
421
287
|
|
|
422
|
-
|
|
423
|
-
// 一次性收集全部内容(不做逐块处理)
|
|
424
|
-
const result = await ai.stream.collect(ai.llm.chatStream({ messages }))
|
|
425
|
-
// { content: string, toolCalls: ToolCall[], finishReason: string | null }
|
|
426
|
-
```
|
|
288
|
+
#### 语音(Audio)
|
|
427
289
|
|
|
428
|
-
|
|
290
|
+
先在 `ai.init()` 中注册语音模型并映射默认识别/合成模型。凭据默认回退到平台环境变量;只有 LLM 与语音模型确认使用同一凭据时,才显式启用 `inheritLlmApiKey`:
|
|
429
291
|
|
|
430
|
-
|
|
292
|
+
平台(`provider`)表示调用协议而非部署位置:`openai` / `mimo` / `qwen` / `doubao` 为云服务;`whisper`(ASR)/ `indextts`(TTS)为可自托管协议,无 canonical 端点,必须显式配置 `baseUrl`,同一协议可切换多个 Endpoint(云 / 内网 / 本地)而不改业务代码。云平台缺少凭据返回 `CONFIGURATION_ERROR`;自托管平台可无凭据(配置 `apiKey` 后按 `Authorization: Bearer` 发送)。
|
|
431
293
|
|
|
432
294
|
```ts
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (result.finishReason === 'tool_calls' && result.toolCalls.length > 0) {
|
|
452
|
-
// 将 assistant 消息(含 tool_calls)追加到对话
|
|
453
|
-
messages.push(processor.toAssistantMessage())
|
|
295
|
+
await ai.init({
|
|
296
|
+
llm: {
|
|
297
|
+
apiKey: process.env.HAI_AI_LLM_APIKEY,
|
|
298
|
+
},
|
|
299
|
+
audio: {
|
|
300
|
+
inheritLlmApiKey: true,
|
|
301
|
+
models: [
|
|
302
|
+
{ id: 'asr', provider: 'qwen', model: 'qwen3-asr-flash-realtime', operations: ['transcribe'] },
|
|
303
|
+
{ id: 'tts', provider: 'qwen', model: 'qwen3-tts-flash-realtime', operations: ['synthesize'] },
|
|
304
|
+
// 自托管:whisper / indextts 必须显式配置 baseUrl
|
|
305
|
+
{ id: 'whisper', provider: 'whisper', model: 'faster-whisper-large-v3', operations: ['transcribe'], baseUrl: 'http://127.0.0.1:8101/v1' },
|
|
306
|
+
{ id: 'indextts', provider: 'indextts', model: 'indextts-2.5', operations: ['synthesize'], baseUrl: 'http://127.0.0.1:8102/v1' },
|
|
307
|
+
],
|
|
308
|
+
transcribeModel: 'asr',
|
|
309
|
+
synthesizeModel: 'tts',
|
|
310
|
+
},
|
|
311
|
+
})
|
|
454
312
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
313
|
+
// 完整识别(可选热词、时间戳粒度与 VAD;strictCapabilities 要求模型真实支持所请求能力)
|
|
314
|
+
const result = await ai.audio.transcribe({
|
|
315
|
+
audio: { data: wavBytes, format: 'wav' },
|
|
316
|
+
language: 'zh',
|
|
317
|
+
contextHints: ['专有名词'],
|
|
318
|
+
timestampGranularities: ['segment', 'word'],
|
|
319
|
+
vad: true,
|
|
320
|
+
model: 'whisper',
|
|
321
|
+
})
|
|
322
|
+
if (result.success) {
|
|
323
|
+
const text = result.data.text
|
|
324
|
+
const language = result.data.language // 实际检测语言
|
|
325
|
+
const words = result.data.segments?.[0]?.words // 词级时间轴(毫秒整数)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 说话人 / 风格参考合成(IndexTTS):speakerReference 表达“谁在说”,styleReference 表达“怎么说”
|
|
329
|
+
const cloned = await ai.audio.synthesize({
|
|
330
|
+
text: '我们下午三点出发。',
|
|
331
|
+
language: 'zh',
|
|
332
|
+
model: 'indextts',
|
|
333
|
+
speakerReference: { audio: speakerWav, language: 'ja' },
|
|
334
|
+
styleReference: { audio: styleWav },
|
|
335
|
+
styleStrength: 0.8,
|
|
336
|
+
targetDurationMs: 3280, // 目标时长(与 speed 互斥),metadata.durationMatched 反馈是否达标
|
|
337
|
+
durationToleranceMs: 120,
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// 实时识别(持续音频输入 → 领域事件流:speech_started / transcript / speech_stopped)
|
|
341
|
+
for await (const event of ai.audio.transcribeStream({
|
|
342
|
+
audio: { chunks: microphoneChunks, format: 'pcm16', sampleRate: 16000 },
|
|
343
|
+
})) {
|
|
344
|
+
if (event.type === 'speech_started')
|
|
345
|
+
onSpeechStart() // 支持服务端 VAD 的平台会在检测到说话时立即产出,可据此取消上游生成
|
|
346
|
+
else if (event.type === 'transcript')
|
|
347
|
+
updateTranscript(event.text, event.final)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 流式合成:调用方为文本段分配稳定 ID,事件可精确关联文本与音频;signal 可随时打断
|
|
351
|
+
const controller = new AbortController()
|
|
352
|
+
for await (const event of ai.audio.synthesizeStream({
|
|
353
|
+
text: { id: 'answer-1', text: '欢迎参加访谈。' },
|
|
354
|
+
voice: 'Cherry',
|
|
355
|
+
instruction: '用轻快的语气',
|
|
356
|
+
signal: controller.signal,
|
|
357
|
+
})) {
|
|
358
|
+
if (event.type === 'segment_started')
|
|
359
|
+
prepareDecoder(event.format, event.sampleRate, event.channels) // 真实输出格式来自服务端解析后的 Provider 输出
|
|
360
|
+
else if (event.type === 'audio')
|
|
361
|
+
await player.write(event.data)
|
|
362
|
+
else if (event.type === 'segment_done')
|
|
363
|
+
markSegmentReadyToCommit(event.segmentId)
|
|
467
364
|
}
|
|
468
|
-
```
|
|
469
365
|
|
|
470
|
-
|
|
366
|
+
// 实时会话启动前按操作校验模型能力
|
|
367
|
+
const caps = ai.audio.getCapabilities({ operation: 'synthesize', model: 'tts' })
|
|
368
|
+
if (caps.success && caps.data.synthesize?.streamingAudioOutput) { /* 可实时 TTS */ }
|
|
369
|
+
```
|
|
471
370
|
|
|
472
|
-
|
|
371
|
+
`ai.audio.synthesize` 返回统一的 `AudioContent` 字节。需要通过 API 返回浏览器可播放的 Base64 负载时,使用 `serializePlayableAudio(result.data)`;该函数会将 `pcm16` 补齐为 WAV 容器,并透传 WAV/MP3:
|
|
473
372
|
|
|
474
373
|
```ts
|
|
475
|
-
|
|
476
|
-
async function* streamToSSE(messages: ChatMessage[]): AsyncGenerator<string> {
|
|
477
|
-
for await (const chunk of ai.llm.chatStream({ messages })) {
|
|
478
|
-
const delta = chunk.choices[0]?.delta?.content
|
|
479
|
-
if (delta) {
|
|
480
|
-
yield ai.stream.encodeSSE({ event: 'delta', data: JSON.stringify({ text: delta }) })
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
yield ai.stream.encodeSSE({ event: 'done', data: '' })
|
|
484
|
-
}
|
|
374
|
+
import { serializePlayableAudio } from '@h-ai/ai'
|
|
485
375
|
|
|
486
|
-
|
|
487
|
-
|
|
376
|
+
const synthesized = await ai.audio.synthesize({ text: '欢迎参加访谈。', voice: 'Cherry' })
|
|
377
|
+
if (!synthesized.success)
|
|
378
|
+
return synthesized
|
|
488
379
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const { text } = JSON.parse(event.data!)
|
|
493
|
-
process.stdout.write(text)
|
|
494
|
-
}
|
|
495
|
-
else if (event.event === 'done') {
|
|
496
|
-
break
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
380
|
+
const playable = serializePlayableAudio(synthesized.data)
|
|
381
|
+
if (!playable.success)
|
|
382
|
+
return playable
|
|
500
383
|
|
|
501
|
-
|
|
502
|
-
decoder.reset()
|
|
384
|
+
return playable.data
|
|
503
385
|
```
|
|
504
386
|
|
|
505
|
-
|
|
387
|
+
`OptionalSecretSchema` 统一用于 LLM、Audio 与 Image 的可选密钥字段:YAML `null`、空字符串和纯空白字符串都会规范化为 `undefined`。语音密钥优先级为模型条目 `apiKey` → 启用继承后的 LLM `apiKey` → 对应平台环境变量;`inheritLlmApiKey` 默认关闭,避免跨供应商误用密钥。
|
|
506
388
|
|
|
507
|
-
|
|
389
|
+
> `synthesizeStream` 严格按 `segment_started → audio* → segment_done` 产出事件。`segment_started` 携带服务端解析 Provider 后的**真实输出音频参数**(`format` / `sampleRate` / `channels`),播放器据此正确解码,不应按请求参数猜测格式。播放器只有在对应音频真正播放完成后才应把该段文本计入 `spokenText`;播放状态仍由应用管理。
|
|
508
390
|
|
|
509
|
-
|
|
391
|
+
取消/超时/连接错误统一为领域错误:`AbortSignal` 触发 → `AUDIO_CANCELLED`(超时 → `AUDIO_TIMEOUT`),连接失败或 `end` 前异常断连 → `AUDIO_CONNECTION_FAILED`。实时连接时长受 `audio.maxStreamDurationMs`(默认 5 分钟)限制。
|
|
510
392
|
|
|
511
|
-
|
|
393
|
+
浏览器 / 移动端通过 `@h-ai/serv` 暴露的统一语音 WebSocket 入口访问,`@h-ai/ai/client` 提供与 Node 端一致的 `audio.*` API(传输细节内部隐藏)。浏览器客户端严格区分正常结束、取消(`AUDIO_CANCELLED`)与异常断连(`AUDIO_CONNECTION_FAILED`):取消或在 `end` 前断连会抛出对应领域错误码,`synthesize` 不会把未完成的部分音频当作成功结果返回。
|
|
512
394
|
|
|
513
|
-
|
|
514
|
-
const result = await ai.embedding.embedText('这是一段需要向量化的文本')
|
|
515
|
-
if (result.success) {
|
|
516
|
-
const vector = result.data // number[](如 1536 维向量)
|
|
517
|
-
}
|
|
518
|
-
```
|
|
395
|
+
IndexTTS 的 Node HTTP 请求使用同版本 Undici fetch/Agent 与 multipart 类型,避免混用 Node 内置 fetch 的调度器协议;模型 `timeout` 覆盖响应头与正文等待。主动取消返回 `AUDIO_CANCELLED`,超时返回 `AUDIO_TIMEOUT`,成功、失败和取消均关闭独立连接;按段流式降级仍在迭代时抛出领域错误。
|
|
519
396
|
|
|
520
|
-
|
|
397
|
+
> 自托管模型服务(faster-whisper / IndexTTS / Qwen3-4B 的 CPU/GPU Docker 镜像、权重下载与离线打包)见 [`models/`](./models/README.md)。镜像与权重下载优先使用 ModelScope,自动回退 HuggingFace 镜像。
|
|
521
398
|
|
|
522
|
-
|
|
523
|
-
const result = await ai.embedding.embedBatch([
|
|
524
|
-
'第一段文本',
|
|
525
|
-
'第二段文本',
|
|
526
|
-
'第三段文本',
|
|
527
|
-
])
|
|
528
|
-
if (result.success) {
|
|
529
|
-
const vectors = result.data // number[][]
|
|
530
|
-
}
|
|
531
|
-
```
|
|
399
|
+
#### 文生图(Image)
|
|
532
400
|
|
|
533
|
-
|
|
401
|
+
在 `ai.init()` 注册模型后,调用方只接收标准化图片字节,不感知厂商 Base64、内联数据或临时 URL:
|
|
534
402
|
|
|
535
403
|
```ts
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
404
|
+
await ai.init({
|
|
405
|
+
image: {
|
|
406
|
+
models: [
|
|
407
|
+
{ id: 'image', provider: 'openai', model: 'gpt-image-2' },
|
|
408
|
+
{ id: 'free', provider: 'pollinations', model: 'zimage' },
|
|
409
|
+
],
|
|
410
|
+
generateModel: 'image',
|
|
411
|
+
},
|
|
540
412
|
})
|
|
541
|
-
if (result.success) {
|
|
542
|
-
const response = result.data
|
|
543
|
-
// response.data: EmbeddingItem[]({ index, embedding })
|
|
544
|
-
// response.usage: { prompt_tokens, total_tokens }
|
|
545
|
-
}
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
---
|
|
549
|
-
|
|
550
|
-
## 推理引擎 — `ai.reasoning`
|
|
551
|
-
|
|
552
|
-
支持三种推理策略:
|
|
553
|
-
|
|
554
|
-
| 策略 | 说明 | 适用场景 |
|
|
555
|
-
| -------------- | -------------------------- | ------------------ |
|
|
556
|
-
| `cot` | 思维链(Chain of Thought) | 分步推理 |
|
|
557
|
-
| `react` | 思考-行动-观察循环 | 需要工具调用的推理 |
|
|
558
|
-
| `plan-execute` | 先规划后执行 | 复杂多步任务 |
|
|
559
413
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
414
|
+
const result = await ai.image.generate({
|
|
415
|
+
prompt: '一个正在构建开源框架的友好机器人',
|
|
416
|
+
size: { width: 1024, height: 1024 },
|
|
417
|
+
referenceImages: [
|
|
418
|
+
{ data: await readImage('character.png'), mimeType: 'image/png' },
|
|
419
|
+
],
|
|
565
420
|
})
|
|
566
|
-
|
|
567
421
|
if (result.success) {
|
|
568
|
-
const {
|
|
569
|
-
|
|
570
|
-
// steps: ReasoningStep[](每步包含 type, content)
|
|
571
|
-
// type: 'thought' | 'action' | 'observation' | 'plan' | 'answer'
|
|
572
|
-
// rounds: 执行轮数
|
|
422
|
+
const { data, mimeType } = result.data.images[0]!
|
|
423
|
+
await saveImage(data, mimeType)
|
|
573
424
|
}
|
|
574
425
|
```
|
|
575
426
|
|
|
576
|
-
|
|
427
|
+
`referenceImages` 可省略;公共层只接受图片字节与 MIME,multipart、`inlineData`、Data URL 等差异由 Provider 转换。凭据可在模型条目传入,或使用 `HAI_AI_IMAGE_<PROVIDER>_APIKEY` / 厂商环境变量。Qwen 与 Seedream 返回的临时 URL 会在 Provider 内立即下载,不会泄漏到公共返回值。完整接口差异与模型说明见 [REFERENCE.md](./REFERENCE.md#image)。
|
|
577
428
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
registry.register(searchTool)
|
|
582
|
-
|
|
583
|
-
const result = await ai.reasoning.run('北京到上海的距离是多少公里?', {
|
|
584
|
-
strategy: 'react',
|
|
585
|
-
tools: registry,
|
|
586
|
-
maxRounds: 5, // 最大轮次(默认 10)
|
|
587
|
-
model: 'gpt-4o',
|
|
588
|
-
temperature: 0.2,
|
|
589
|
-
systemPrompt: '你是一个地理知识专家',
|
|
590
|
-
})
|
|
591
|
-
```
|
|
429
|
+
图片下载只允许无凭证、默认 443 端口的公网 HTTPS,每次 socket DNS 解析校验全部地址并直接使用该批地址;阻止 IPv4/IPv6 私网、回环、链路本地、映射/转换及保留地址,重定向最多 3 次且每跳重新校验。下载使用独立连接,不传递厂商认证头或应用 Cookie。`baseUrl` 是可信运维配置,厂商 API 请求拒绝重定向。
|
|
430
|
+
|
|
431
|
+
`image.maxImageBytes` 默认 20971520(20 MiB,最大 100 MiB),限制单张输出和参考图;`image.maxImages` 默认 4(最大 16),限制输出与参考图数量,临时 URL 串行下载。实际读取(含解压后)超过上限立即取消;JSON 响应最多 `max(64 KiB, 2 × maxImageBytes)`,错误正文最多 64 KiB。模型 `timeout` 默认 120000ms,覆盖厂商请求、重定向和全部下载;`signal` 可取消整个过程。输出仅支持 PNG/JPEG/GIF/WebP 的 MIME 与魔数匹配,拒绝 SVG;魔数校验不等于完整图片解码或尺寸核验。拒绝地址、超限、超时分别返回 `IMAGE_DOWNLOAD_BLOCKED`、`IMAGE_TOO_LARGE`、`IMAGE_TIMEOUT`,调用方须先检查 success。
|
|
592
432
|
|
|
593
|
-
|
|
433
|
+
#### Context 管理器
|
|
594
434
|
|
|
595
|
-
|
|
435
|
+
自动记忆提取在本节点后台运行;`pendingMemoryTasks=0` 不代表全部成功。`flush()` 等待本批任务,异常和失败 HaiResult 都返回 `MEMORY_EXTRACT_FAILED`(含失败数量),同轮并发观察者共享结果。返回即确认该批报告,后续只观察新任务,不自动重放可能部分完成的提取。`save()`、`reset({ waitForMemoryTasks: true })`、`consolidate()` 遇到该失败立即返回,不继续后续状态或记忆写入。进程崩溃后的外部效果仍须人工核对。
|
|
596
436
|
|
|
597
|
-
|
|
437
|
+
`scope: { objectId, sessionId }` 必填。相同 scope 的所有节点共用一条版本记录,原子保存消息、摘要、生成 owner、轮次及待提交状态;`addMessage`、每个对外流式 delta 和最终提交即时持久化,`save()` 只刷新共享快照、等待本节点记忆任务并同步会话目录。`getMessages/getTurns/getSummaries/getTokenUsage` 返回最近一次成功读写的独立快照;跨节点读取前调用 `await manager.reload()` 或 `restoreManager()`。
|
|
598
438
|
|
|
599
|
-
|
|
439
|
+
并发限制按共享 scope 生效;`reject` 立即返回忙,`queue` 可取消地轮询(不承诺 FIFO)。生成租约为存储端时钟 30 秒,每 10 秒续租,最终写入同时校验版本、owner 和期限。失联生成过期后由下一次操作标为 interrupted,保留生成记录,不自动重放 LLM、工具或记忆提取。manual 的 pending 不自动过期,可在另一节点/重启后按同一 turnId 提交;相同终态与文本重复提交幂等,不同内容拒绝。外部工具/LLM/后台记忆不与上下文跨系统原子提交,进程失联后须核对未知副作用,不能据此承诺外部恰好一次。`reset` 更换共享纪元;`removeSession` 清空正文并保留永久墓碑,同一 scope 不复用,旧节点不能重建。
|
|
600
440
|
|
|
601
|
-
|
|
441
|
+
自定义 `AIRelStore` 必须提供 `readVersioned`、`compareAndSwap` 和存储端 `currentTime`;普通 save/saveMany 也要更换不可复用版本。CAS 的 `expectedVersion: undefined` 仅创建,`notAfter` 在最终写入以存储时钟严格检查,冲突/截止返回 undefined,存储故障抛错。默认 PostgreSQL/MySQL/SQLite 实现均执行这些条件;SQLite 仅供开发/测试,生产使用 PostgreSQL/MySQL。新 schema 含 version 列,不提供旧表自动迁移。
|
|
602
442
|
|
|
603
443
|
```ts
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
{ id: 'faq', collection: 'faq', name: '常见问题' },
|
|
610
|
-
],
|
|
611
|
-
},
|
|
444
|
+
const manager = ai.context.createManager({
|
|
445
|
+
scope: { objectId: 'user-001', sessionId: 'sess-001' },
|
|
446
|
+
compress: { auto: true, strategy: 'hybrid', maxTokens: 8000 },
|
|
447
|
+
memory: { enable: true, enableExtract: true },
|
|
448
|
+
concurrency: 'reject', // 单活动生成(默认):活动生成期间的新 chat 返回 CONTEXT_BUSY;'queue' 则排队
|
|
612
449
|
})
|
|
450
|
+
if (manager.success) {
|
|
451
|
+
const reply = await manager.data.chat('你好')
|
|
452
|
+
const saved = await manager.data.save()
|
|
453
|
+
if (!saved.success)
|
|
454
|
+
return saved
|
|
455
|
+
// reset 生命周期完整:终止活动生成、清空消息/摘要/轮次,默认保留系统提示词
|
|
456
|
+
await manager.data.reset() // 可传 { preserveSystemPrompt, cancelActiveTurn, waitForMemoryTasks }
|
|
457
|
+
}
|
|
613
458
|
```
|
|
614
459
|
|
|
615
|
-
|
|
460
|
+
同一 scope 默认实行**跨节点单活动生成**,避免「上一轮 AI 尚未退出,下一轮 user 消息先写入」导致的消息乱序;`reset()` 现为异步并会终止活动生成、释放并发屏障、清空轮次并默认重新写入 Persona/System Prompt。
|
|
616
461
|
|
|
617
|
-
|
|
618
|
-
// 添加检索源(持久化到 DB)
|
|
619
|
-
await ai.retrieval.addSource({
|
|
620
|
-
id: 'docs',
|
|
621
|
-
collection: 'documentation',
|
|
622
|
-
name: '产品文档',
|
|
623
|
-
topK: 5,
|
|
624
|
-
minScore: 0.7,
|
|
625
|
-
})
|
|
462
|
+
##### 真实对话状态(Conversation Commit Layer)
|
|
626
463
|
|
|
627
|
-
|
|
628
|
-
id: 'faq',
|
|
629
|
-
collection: 'faq',
|
|
630
|
-
name: '常见问题',
|
|
631
|
-
})
|
|
464
|
+
默认(`turnCommit: 'auto'`)下,`chat` / `chatStream` 会把**模型生成的完整文本**写入上下文。但在「模型生成 → TTS 合成 → 实际播放」链路中,AI 可能说到一半就被打断——此时进入下一轮所有参与者可见的对话状态,应当是**实际播放出去的部分**,而非模型本想说完的全文。
|
|
632
465
|
|
|
633
|
-
|
|
634
|
-
const sources = await ai.retrieval.listSources() // RetrievalSource[]
|
|
635
|
-
```
|
|
466
|
+
设置 `turnCommit: 'manual'` 后,生成结果不会自动写入上下文,而是返回一个 `turnId`;由调用方在确定「实际发生了什么」后显式提交真实文本。
|
|
636
467
|
|
|
637
|
-
|
|
468
|
+
`chatStream` 在**调用上游模型前**就登记轮次并产出 `turn_started`(事件序列 `turn_started → delta* → done`,中途取消时 `turn_started → delta* → cancelled`)。因此即使生成到一半被 `AbortSignal` 取消,也能拿到 `turnId` 与已生成文本,用真实内容提交:
|
|
638
469
|
|
|
639
470
|
```ts
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
})
|
|
471
|
+
const created = ai.context.createManager({ scope: { objectId: 'user-001', sessionId: 'sess-001' }, turnCommit: 'manual' })
|
|
472
|
+
if (!created.success)
|
|
473
|
+
return created
|
|
474
|
+
const m = created.data
|
|
475
|
+
const controller = new AbortController()
|
|
646
476
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
477
|
+
for await (const ev of m.chatStream('请展开讲讲', { signal: controller.signal })) {
|
|
478
|
+
if (ev.type === 'turn_started') {
|
|
479
|
+
await m.markTurnSpeaking(ev.turnId) // 可选:标记进入播放
|
|
480
|
+
}
|
|
481
|
+
else if (ev.type === 'delta') {
|
|
482
|
+
feedTts(ev.text) // 边生成边合成播放
|
|
483
|
+
}
|
|
484
|
+
else if (ev.type === 'done') {
|
|
485
|
+
await m.commitTurn(ev.turnId) // 完整提交
|
|
486
|
+
}
|
|
487
|
+
else if (ev.type === 'cancelled') {
|
|
488
|
+
// 生成被 controller.abort() 取消:轮次保留,只提交实际播放出去的部分
|
|
489
|
+
await m.interruptTurn(ev.turnId, { text: actuallySpokenText })
|
|
653
490
|
}
|
|
654
491
|
}
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
### 移除检索源
|
|
658
492
|
|
|
659
|
-
|
|
660
|
-
|
|
493
|
+
// 观测每一轮的 generated / committed / status
|
|
494
|
+
const turns = m.getTurns()
|
|
661
495
|
```
|
|
662
496
|
|
|
663
|
-
|
|
497
|
+
- `commitTurn(turnId, { text? })` — 提交真实文本(缺省用完整生成文本),状态转 `completed`。
|
|
498
|
+
- `interruptTurn(turnId, { text? })` — 只写入实际表达出去的部分(缺省视为未表达,不写入),状态转 `interrupted`。
|
|
499
|
+
- 只有 `committed` 的内容进入上下文与记忆提取;未提交/被打断丢弃的部分不会污染后续轮次。
|
|
500
|
+
- 若轮次在流完成前已被 `interruptTurn` 打断(如主持人抢话,同时上游模型恰好正常结束),`chatStream` **不会再产出 `done`**,避免业务层误判为正常完成后继续提交文本。
|
|
664
501
|
|
|
665
|
-
|
|
502
|
+
##### 会话固化(Memory 生命周期)
|
|
666
503
|
|
|
667
|
-
|
|
504
|
+
会话进行中,每轮对话按 `scope: { sessionId }` 提取**短期会话记忆**;会话结束时用 `consolidate()`
|
|
505
|
+
把「短期记忆 + 摘要」沉淀为**跨会话长期记忆**,形成 `Session Memory → Summary → Long-term Memory` 闭环:
|
|
668
506
|
|
|
669
507
|
```ts
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
topK: 5,
|
|
673
|
-
minScore: 0.6,
|
|
674
|
-
model: 'gpt-4o-mini',
|
|
675
|
-
temperature: 0.3,
|
|
676
|
-
systemPrompt: '根据检索到的文档内容回答问题,注明信息来源。',
|
|
677
|
-
})
|
|
678
|
-
|
|
508
|
+
// 会话结束时固化:整合摘要 → 提取长期记忆(写入不含 sessionId 的持久作用域)
|
|
509
|
+
const result = await manager.consolidate({ scope: { userId: 'user-001', personaId: 'xiaoq' } })
|
|
679
510
|
if (result.success) {
|
|
680
|
-
|
|
681
|
-
//
|
|
682
|
-
// citations: Citation[](引用来源列表)
|
|
683
|
-
// context: RagContextItem[](使用的上下文片段)
|
|
511
|
+
// result.data.summary —— 本次会话整合摘要
|
|
512
|
+
// result.data.memories —— 固化到长期记忆的条目
|
|
684
513
|
}
|
|
685
514
|
```
|
|
686
515
|
|
|
687
|
-
|
|
516
|
+
#### Persona(AI 角色人格)
|
|
517
|
+
|
|
518
|
+
Memory 用 `objectId` / `scope` 回答「谁的记忆」;Persona 回答「AI 是谁」——为每个 AI 角色定义
|
|
519
|
+
稳定的系统提示词与性格特征,并通过 `scope: { personaId }` 关联其长期记忆:
|
|
688
520
|
|
|
689
521
|
```ts
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
},
|
|
522
|
+
await ai.persona.save({
|
|
523
|
+
id: 'xiaoq',
|
|
524
|
+
name: '小Q',
|
|
525
|
+
systemPrompt: '你是一位经济学家,善于从长期视角分析问题。',
|
|
526
|
+
traits: ['数据驱动', '偏好引用真实案例'],
|
|
696
527
|
})
|
|
697
|
-
```
|
|
698
528
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
{ role: 'assistant', content: '部署分为三步:1. 构建镜像 2. 推送仓库 3. 更新集群' },
|
|
706
|
-
],
|
|
529
|
+
// 组合出可直接喂给 ContextManager 的系统提示词(systemPrompt + traits)
|
|
530
|
+
const composed = await ai.persona.compose('xiaoq')
|
|
531
|
+
const manager = ai.context.createManager({
|
|
532
|
+
scope: { objectId: 'user-001', sessionId: 'persona-session' },
|
|
533
|
+
systemPrompt: composed.data,
|
|
534
|
+
memory: { enable: true, enableExtract: true, scope: { personaId: 'xiaoq' } },
|
|
707
535
|
})
|
|
708
536
|
```
|
|
709
537
|
|
|
710
|
-
|
|
538
|
+
`ai.persona` 提供 `save` / `get` / `update` / `remove` / `list` / `compose`;角色档案全局共享(不按 `objectId` 隔离)。
|
|
539
|
+
|
|
540
|
+
### 配置
|
|
711
541
|
|
|
712
|
-
|
|
542
|
+
```yaml
|
|
543
|
+
llm:
|
|
544
|
+
apiKey: ''
|
|
545
|
+
baseUrl: https://api.openai.com/v1
|
|
546
|
+
model: gpt-4o-mini
|
|
547
|
+
api: chat # chat(默认)| responses | anthropic —— 底层 API 协议,对使用方透明
|
|
548
|
+
timeout: 60000
|
|
549
|
+
tempModelCacheTtl: 600000 # 临时模型客户端缓存 TTL(毫秒,默认 10 分钟)
|
|
550
|
+
models: # 可为每个模型单独指定协议
|
|
551
|
+
- {id: fast, model: gpt-4o-mini}
|
|
552
|
+
- {id: strong, model: gpt-4.1, api: responses}
|
|
553
|
+
- {id: claude, model: claude-3-5-sonnet-latest, api: anthropic}
|
|
554
|
+
scenarios:
|
|
555
|
+
chat: fast
|
|
556
|
+
reasoning: strong
|
|
557
|
+
embedding: embed
|
|
713
558
|
|
|
714
|
-
|
|
559
|
+
embedding:
|
|
560
|
+
dimensions: 1536
|
|
561
|
+
batchSize: 100
|
|
715
562
|
|
|
716
|
-
|
|
563
|
+
knowledge:
|
|
564
|
+
collection: hai_ai_knowledge
|
|
565
|
+
dimension: 1536
|
|
566
|
+
cleanOptions:
|
|
567
|
+
removeHtml: true
|
|
568
|
+
normalizeWhitespace: true
|
|
569
|
+
chunkOptions:
|
|
570
|
+
mode: markdown
|
|
571
|
+
maxSize: 1500
|
|
572
|
+
overlap: 200
|
|
717
573
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
574
|
+
memory:
|
|
575
|
+
provider: native # native | mem0
|
|
576
|
+
maxEntriesPerObject: 1000 # 单主体(objectId)最大记忆条数
|
|
577
|
+
maxEntriesGlobal: 100000 # 跨所有主体的全局上限
|
|
578
|
+
recencyDecay: 0.95
|
|
579
|
+
embeddingEnabled: true
|
|
580
|
+
defaultTopK: 10
|
|
581
|
+
candidateMultiplier: 5 # 候选池倍数:先取回 topK×倍数 条候选,再按 scope/重要性过滤,最后截取 topK
|
|
582
|
+
writebackRelatedTopK: 20
|
|
723
583
|
```
|
|
724
584
|
|
|
725
|
-
|
|
585
|
+
通过 `core.config.load('ai', ...)` 加载时,约定环境变量优先于 YAML;
|
|
586
|
+
例如 `HAI_AI_LLM_APIKEY`、`HAI_AI_LLM_BASEURL`、`HAI_AI_LLM_MODEL`。
|
|
726
587
|
|
|
727
|
-
|
|
728
|
-
const result = await ai.knowledge.ingest({
|
|
729
|
-
documentId: 'doc-001',
|
|
730
|
-
content: markdownContent,
|
|
731
|
-
title: '产品使用手册',
|
|
732
|
-
url: 'https://docs.example.com/manual',
|
|
733
|
-
enableEntityExtraction: true, // 自动提取人物、项目、概念等实体
|
|
734
|
-
cleanOptions: {
|
|
735
|
-
removeHtml: true,
|
|
736
|
-
},
|
|
737
|
-
chunkOptions: {
|
|
738
|
-
mode: 'markdown', // sentence | paragraph | markdown | page | custom
|
|
739
|
-
maxSize: 1500,
|
|
740
|
-
overlap: 200,
|
|
741
|
-
},
|
|
742
|
-
metadata: { category: 'manual', version: '2.0' },
|
|
743
|
-
})
|
|
588
|
+
启用 mem0(真·嵌入式 mem0ai/oss 引擎):
|
|
744
589
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
// duration: 耗时(毫秒)
|
|
750
|
-
}
|
|
590
|
+
```yaml
|
|
591
|
+
memory:
|
|
592
|
+
provider: mem0
|
|
593
|
+
defaultTopK: 10
|
|
751
594
|
```
|
|
752
595
|
|
|
753
|
-
|
|
596
|
+
- **`native`(默认,推荐)**:HAI 原生引擎,复用同一套 vecdb(向量库)、reldb(关系库)、LLM 与 Embedding。`extract` 采用 **Mem0 式批量合并**——一次 LLM 调用对整批抽取事实与相关既有记忆做 ADD / UPDATE / DELETE / NONE 决策,实现增量更新、跨条去重与矛盾删除,并支持 `category` 主题标签。`maxEntriesPerObject`、`maxEntriesGlobal`、`recencyDecay`、`embeddingEnabled`、`writebackRelatedTopK` 均作用于此后端;淘汰按 `objectId` 分区触发,不会因某一主体写入过多而淘汰其他主体的记忆。native 后端的 `scope` 过滤:候选集已被 `objectId` 索引收窄(≤ `maxEntriesPerObject`),PostgreSQL 上还会把 scope 下推为 `data @> '{"scope":...}'::jsonb` 包含查询并命中 JSONB **GIN 索引**(SQLite / MySQL 退回内存匹配,结果一致)。
|
|
597
|
+
- **`mem0`(真·mem0ai/oss)**:直接使用 `mem0ai/oss` 的 `Memory` 引擎(嵌入式,无云服务)。LLM / Embedder 从 `llm` 配置提取;`qdrant` 可复用底层 vecdb。当前适配仅映射带完整连接参数的 `qdrant`;生产环境(`NODE_ENV` 或 `HAI_CORE_ENV` 为 `production`)即使设置 `memory.allowEphemeralFallback: true` 也拒绝实际内存回退。临时回退仅限开发或明确的 `HAI_E2E=1` / `HAI_E2E_MOCK=1`;缺少后端同样不能在生产启动。历史记录默认禁用。
|
|
754
598
|
|
|
755
|
-
|
|
756
|
-
const result = await ai.knowledge.retrieve('数据库配置方法', {
|
|
757
|
-
topK: 10,
|
|
758
|
-
minScore: 0.6,
|
|
759
|
-
enableEntityBoost: true, // 实体命中时加权(提高相关性)
|
|
760
|
-
collection: 'my-knowledge',
|
|
761
|
-
})
|
|
599
|
+
两个 Provider 对外 `ai.memory.*` API 完全一致(`extract` / `recall` / `injectMemories` / `add` / `update` / `get` / `remove` / `list` / `listPage` / `clear`),均支持 `objectId`(主体隔离)与 `scope`(业务作用域 key-value 过滤,如 `{ topicId, personaId }`)。`recall` / `list` / `listPage` / `clear` 均按 `scope` 严格过滤,`clear` 在传入 `types` / `scope` 时仅删除同时匹配项(避免误删)。mem0 后端的 `extract` 在框架层用统一提取器完成分类与打分(honor `types` / `model` / `minImportance` / `systemPrompt`)后以 `infer:false` 写入,保留 `hai_type` / `hai_importance`;`recall` 同样支持 `types` 过滤与 `recencyWeight` 时间衰减——二者行为与 native 一致。Mem0 的 `update` 使用 SDK 原地修改文本与元数据,保持 `id` 和创建时间。SDK 查询使用 `filters.user_id`,写入和整主体删除使用 `userId`。SDK 无列表游标:`list` / `listPage` / 条件 `clear` 最多支持每主体 1000 条原始候选(含过期条目),多取一条探测完整性;超限返回 `MEMORY_LIST_INCOMPLETE`(906),不会伪造 total 或执行部分条件删除。候选完整时按 UTC 日期剔除过期项,再按类型/scope 过滤和分页;仅 `objectId` 的清理使用 SDK `deleteAll`,不受列表上限限制。公共空条件清理被拒绝;跨主体全清仅限受信管理员显式 `admin.clearAll({ confirm: true })`。
|
|
762
600
|
|
|
763
|
-
|
|
764
|
-
for (const item of result.data.items) {
|
|
765
|
-
// item.content — 匹配的文本
|
|
766
|
-
// item.score — 综合分数
|
|
767
|
-
// item.citation — 引用(documentId, title, url)
|
|
768
|
-
// item.matchedEntities — 命中的实体名称
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
```
|
|
601
|
+
Mem0 3.3.1 的 Qdrant 适配仍依赖 `QdrantClient.search`;项目将 `@qdrant/js-client-rest` 精确锁定为受测的 `1.18.0`,AI 的可选 peer 也声明此版本。启动会检查该公开方法,缺失时明确配置失败;不要覆盖为已移除此方法的 1.19。
|
|
772
602
|
|
|
773
|
-
|
|
603
|
+
适配器通过 SDK 公开的 `client` 配置注入独享客户端;Mem0 的 Qdrant 写入与删除均使用 `wait: true`,等待服务端应用完成后才继续写后读或分批清理。
|
|
774
604
|
|
|
775
|
-
|
|
776
|
-
const result = await ai.knowledge.ask('如何配置 Redis 缓存?', {
|
|
777
|
-
topK: 5,
|
|
778
|
-
model: 'gpt-4o-mini',
|
|
779
|
-
temperature: 0.3,
|
|
780
|
-
systemPrompt: '基于提供的知识回答,标注信息来源。',
|
|
781
|
-
})
|
|
605
|
+
Mem0 的 pgvector 组合在所有环境拒绝启动:当前 SDK 将 PostgreSQL 连接保存在私有对象中,未提供公开关闭入口,框架无法履行资源清理契约。需要 pgvector 时选择 `memory.provider: native`;该原生后端保持支持。`disableHistory: true` 使用 SDK 无资源的历史占位实现,不创建历史 SQLite 文件。工厂通过公开的一条有界只读查询等待实际后端 ready 后才发布成功。
|
|
782
606
|
|
|
783
|
-
|
|
784
|
-
const { answer, citations, usage } = result.data
|
|
785
|
-
// answer: LLM 生成的回答
|
|
786
|
-
// citations: Citation[](引用列表)
|
|
787
|
-
// usage: { prompt_tokens, completion_tokens, total_tokens }
|
|
788
|
-
}
|
|
789
|
-
```
|
|
607
|
+
`add` / `extract` 的 SDK 回执不含完整业务字段,框架写后读取权威记录再返回;并发提取等待全部在途写入结束。若写入或写后读失败,`error.ext.requiresEffectReconciliation=true`,`memoryIds` 保存已知写入 ID(回执丢失时可能为空)。应按主体核对实际记忆后决定恢复,不能盲目重做 add/extract;框架不自动重放。
|
|
790
608
|
|
|
791
|
-
|
|
609
|
+
**候选池与 scope 漏召回**:`scope` 过滤在内存中完成,若先按 `topK` 截断再过滤,同一主体下相关度较高的其它主题/角色记忆会把目标 scope 的记忆挤出候选池,导致「明明有却召回 0 条」。为此 `recall` / `injectMemories` 先取回 `topK × candidateMultiplier`(默认 5)条候选,过滤后再截取 `topK`。scope 隔离越细(如按 `topicId` + `personaId`),可将 `candidateMultiplier` 调大:
|
|
792
610
|
|
|
793
611
|
```ts
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
612
|
+
const memories = await ai.memory.recall('经济发展', {
|
|
613
|
+
objectId: 'user-001',
|
|
614
|
+
scope: { topicId: 'C' },
|
|
615
|
+
topK: 10,
|
|
616
|
+
candidateMultiplier: 8, // 覆盖配置默认值,扩大候选池
|
|
799
617
|
})
|
|
800
618
|
```
|
|
801
619
|
|
|
802
|
-
|
|
620
|
+
`ai.config` 返回脱敏后的配置快照;`apiKey`、`privateKey`、URL 内嵌凭证等敏感字段不会原样暴露。
|
|
803
621
|
|
|
804
|
-
|
|
805
|
-
// 列出所有实体
|
|
806
|
-
const entities = await ai.knowledge.listEntities({
|
|
807
|
-
type: 'person', // person | project | concept | organization | location | event | other
|
|
808
|
-
keyword: '张',
|
|
809
|
-
limit: 50,
|
|
810
|
-
})
|
|
622
|
+
### 安全边界
|
|
811
623
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
})
|
|
817
|
-
if (result.success) {
|
|
818
|
-
// result.data: EntityDocumentResult[]
|
|
819
|
-
}
|
|
820
|
-
```
|
|
624
|
+
- Prompt、检索文档和模型输出都按不可信输入处理;不要把用户内容拼进不可覆盖的系统规则。
|
|
625
|
+
- Zod 只校验工具参数形状,不代表调用者有权限。高权限工具必须在 handler 内再次校验身份、租户、资源归属与配额,并只把允许自动执行的工具注册给模型。
|
|
626
|
+
- `callRemoteAgent()` 是独立客户端能力,只依赖 `ai.init()`,不要求配置 Agent Card 或注册本地 executor。它拒绝非 HTTP(S) 和 URL 内嵌凭据,但应用仍必须对远端 origin 配置白名单,并在出口代理处限制 DNS 重绑定、重定向到私网和云元数据地址。
|
|
627
|
+
- 不记录完整 Prompt、工具参数、A2A headers、临时模型凭据或带 query 的远端 URL;确需审计时仅保存脱敏摘要。
|
|
821
628
|
|
|
822
|
-
|
|
629
|
+
### 错误处理
|
|
823
630
|
|
|
824
|
-
|
|
631
|
+
```ts
|
|
632
|
+
const result = await ai.llm.chat({ messages })
|
|
633
|
+
if (!result.success) {
|
|
634
|
+
switch (result.error.code) {
|
|
635
|
+
case HaiAIError.NOT_INITIALIZED.code:
|
|
636
|
+
// 先调用 ai.init()
|
|
637
|
+
break
|
|
638
|
+
case HaiAIError.API_ERROR.code:
|
|
639
|
+
// 上游模型服务失败,可重试或降级
|
|
640
|
+
break
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
```
|
|
825
644
|
|
|
826
|
-
|
|
645
|
+
常见错误段位:
|
|
827
646
|
|
|
828
|
-
|
|
647
|
+
- `hai:ai:010-012`:初始化/配置。
|
|
648
|
+
- `hai:ai:100-107`:LLM 与历史记录。
|
|
649
|
+
- `hai:ai:300-302`:Embedding。
|
|
650
|
+
- `hai:ai:600-701`:Retrieval/RAG。
|
|
651
|
+
- `hai:ai:800-805`:Knowledge。
|
|
652
|
+
- `hai:ai:050-059`:Audio。
|
|
653
|
+
- `hai:ai:060-068`:Image,含下载地址拒绝、资源超限与总超时。
|
|
654
|
+
- `hai:ai:900-906`:Memory。
|
|
655
|
+
- `hai:ai:980-984`:A2A。
|
|
829
656
|
|
|
830
|
-
|
|
831
|
-
| ------------- | -------------------- |
|
|
832
|
-
| `fact` | 事实信息 |
|
|
833
|
-
| `preference` | 用户偏好、习惯 |
|
|
834
|
-
| `event` | 重要事件、截止日期 |
|
|
835
|
-
| `entity` | 关键人物、组织、项目 |
|
|
836
|
-
| `instruction` | 用户的明确指令或规则 |
|
|
657
|
+
## 开发测试
|
|
837
658
|
|
|
838
|
-
|
|
659
|
+
```bash
|
|
660
|
+
pnpm --filter @h-ai/ai typecheck
|
|
661
|
+
pnpm --filter @h-ai/ai lint
|
|
662
|
+
pnpm --filter @h-ai/ai test
|
|
663
|
+
pnpm --filter @h-ai/ai build
|
|
664
|
+
```
|
|
839
665
|
|
|
840
|
-
|
|
841
|
-
const messages = [
|
|
842
|
-
{ role: 'user' as const, content: '我叫张三,我更喜欢用中文回复,我的项目叫 HAI Framework' },
|
|
843
|
-
{ role: 'assistant' as const, content: '好的张三,我记住了你的偏好。' },
|
|
844
|
-
]
|
|
845
|
-
|
|
846
|
-
const result = await ai.memory.extract(messages, {
|
|
847
|
-
types: ['preference', 'entity'], // 只提取指定类型(可选)
|
|
848
|
-
minImportance: 0.5, // 过滤低重要性条目(可选)
|
|
849
|
-
objectId: 'user-001', // 关联到对象标识(可选,不传为全局)
|
|
850
|
-
model: 'gpt-4o-mini', // 指定提取模型(可选)
|
|
851
|
-
systemPrompt: 'Only extract durable user preferences and explicit long-term instructions.', // 自定义提取提示词(可选)
|
|
852
|
-
})
|
|
853
|
-
|
|
854
|
-
if (result.success) {
|
|
855
|
-
// result.data: MemoryEntry[](已自动存储到内存)
|
|
856
|
-
// 每条包含:id, content, type, importance, vector, createdAt 等
|
|
857
|
-
}
|
|
858
|
-
```
|
|
859
|
-
|
|
860
|
-
### 手动添加记忆
|
|
861
|
-
|
|
862
|
-
```ts
|
|
863
|
-
const result = await ai.memory.add({
|
|
864
|
-
content: '用户是后端开发工程师,使用 TypeScript',
|
|
865
|
-
type: 'fact',
|
|
866
|
-
importance: 0.8, // [0, 1],默认 0.5
|
|
867
|
-
objectId: 'user-001', // 可选,不传为全局
|
|
868
|
-
metadata: { tags: ['tech', 'profile'] },
|
|
869
|
-
})
|
|
870
|
-
|
|
871
|
-
if (result.success) {
|
|
872
|
-
const entry = result.data // MemoryEntry(含自动生成的 id 和 vector)
|
|
873
|
-
}
|
|
874
|
-
```
|
|
875
|
-
|
|
876
|
-
### 更新记忆
|
|
877
|
-
|
|
878
|
-
```ts
|
|
879
|
-
const result = await ai.memory.update('mem_1709712345_abc12345', {
|
|
880
|
-
content: '用户是全栈开发工程师',
|
|
881
|
-
importance: 0.9,
|
|
882
|
-
})
|
|
883
|
-
if (result.success) {
|
|
884
|
-
// result.data: MemoryEntry(更新后的条目)
|
|
885
|
-
}
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
### 检索记忆
|
|
889
|
-
|
|
890
|
-
```ts
|
|
891
|
-
const result = await ai.memory.recall('用户的编程语言偏好', {
|
|
892
|
-
topK: 5, // 返回数量(默认 10)
|
|
893
|
-
types: ['preference'], // 按类型过滤
|
|
894
|
-
minImportance: 0.3, // 最低重要性
|
|
895
|
-
recencyWeight: 0.2, // 时间权重 [0, 1](0 = 纯相似度,1 = 纯时间排序)
|
|
896
|
-
objectId: 'user-001', // 按对象过滤(可选)
|
|
897
|
-
})
|
|
898
|
-
|
|
899
|
-
if (result.success) {
|
|
900
|
-
for (const memory of result.data) {
|
|
901
|
-
// memory.content — 记忆内容
|
|
902
|
-
// memory.type — 类型
|
|
903
|
-
// memory.importance — 重要性
|
|
904
|
-
// memory.createdAt — 创建时间
|
|
905
|
-
// memory.accessCount — 被检索次数
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
```
|
|
909
|
-
|
|
910
|
-
### 注入记忆到对话
|
|
911
|
-
|
|
912
|
-
```ts
|
|
913
|
-
const messages = [
|
|
914
|
-
{ role: 'system' as const, content: '你是一个编程助手' },
|
|
915
|
-
{ role: 'user' as const, content: '帮我写一个排序函数' },
|
|
916
|
-
]
|
|
917
|
-
|
|
918
|
-
const result = await ai.memory.injectMemories(messages, {
|
|
919
|
-
topK: 5, // 注入记忆数量
|
|
920
|
-
maxTokens: 500, // 记忆最大 Token 预算
|
|
921
|
-
position: 'system', // 'system' = 追加到 system 末尾,'before-last' = 插入最后一条用户消息之前
|
|
922
|
-
objectId: 'user-001', // 按对象过滤(可选)
|
|
923
|
-
})
|
|
924
|
-
|
|
925
|
-
if (result.success) {
|
|
926
|
-
// result.data: ChatMessage[](已注入记忆的消息列表)
|
|
927
|
-
// system 消息末尾会追加:
|
|
928
|
-
// --- Relevant Memories ---
|
|
929
|
-
// [1] (preference) 用户偏好使用 TypeScript
|
|
930
|
-
// [2] (fact) 用户是后端工程师
|
|
931
|
-
// --- End Memories ---
|
|
932
|
-
|
|
933
|
-
const response = await ai.llm.chat({ messages: result.data })
|
|
934
|
-
}
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
### 列表、删除与清空
|
|
938
|
-
|
|
939
|
-
```ts
|
|
940
|
-
// 列出记忆(支持过滤)
|
|
941
|
-
const list = await ai.memory.list({
|
|
942
|
-
types: ['fact', 'preference'],
|
|
943
|
-
objectId: 'user-001',
|
|
944
|
-
limit: 50,
|
|
945
|
-
})
|
|
946
|
-
|
|
947
|
-
// 分页列出记忆
|
|
948
|
-
const page = await ai.memory.listPage({
|
|
949
|
-
objectId: 'user-001',
|
|
950
|
-
offset: 0,
|
|
951
|
-
limit: 20,
|
|
952
|
-
})
|
|
953
|
-
if (page.success) {
|
|
954
|
-
// page.data.items: MemoryEntry[]
|
|
955
|
-
// page.data.total: number
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// 删除指定记忆
|
|
959
|
-
await ai.memory.remove('mem_1709712345_abc12345')
|
|
960
|
-
|
|
961
|
-
// 按类型清空
|
|
962
|
-
await ai.memory.clear({ types: ['event'] })
|
|
963
|
-
|
|
964
|
-
// 按对象清空
|
|
965
|
-
await ai.memory.clear({ objectId: 'user-001' })
|
|
966
|
-
|
|
967
|
-
// 全部清空
|
|
968
|
-
await ai.memory.clear()
|
|
969
|
-
```
|
|
970
|
-
|
|
971
|
-
---
|
|
972
|
-
|
|
973
|
-
## Token 估算 — `ai.token`
|
|
974
|
-
|
|
975
|
-
估算文本或消息列表的 Token 数量。
|
|
976
|
-
|
|
977
|
-
```ts
|
|
978
|
-
// 估算消息列表的 Token 数
|
|
979
|
-
const tokenCount = ai.token.estimateMessages(messages)
|
|
980
|
-
// tokenCount: number
|
|
981
|
-
|
|
982
|
-
// 估算纯文本的 Token 数
|
|
983
|
-
const count = ai.token.estimateText('Hello world')
|
|
984
|
-
// count: number(中文约 1.5 token/字,英文约 4 字符/token)
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
---
|
|
988
|
-
|
|
989
|
-
## 消息摘要 — `ai.summary`
|
|
990
|
-
|
|
991
|
-
对消息列表生成 LLM 摘要,支持增量追加。
|
|
992
|
-
|
|
993
|
-
```ts
|
|
994
|
-
// 对消息生成摘要
|
|
995
|
-
const result = await ai.summary.summarize(messages, {
|
|
996
|
-
model: 'gpt-4o-mini',
|
|
997
|
-
temperature: 0.3,
|
|
998
|
-
systemPrompt: 'Focus on key decisions, constraints, and unresolved action items.',
|
|
999
|
-
})
|
|
1000
|
-
|
|
1001
|
-
if (result.success) {
|
|
1002
|
-
const { summary, tokenCount, coveredMessages } = result.data
|
|
1003
|
-
// summary: 摘要文本
|
|
1004
|
-
// tokenCount: 摘要的估算 Token 数
|
|
1005
|
-
// coveredMessages: 覆盖的原始消息数
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// 生成纯文本摘要
|
|
1009
|
-
const textResult = await ai.summary.generate(messages, {
|
|
1010
|
-
systemPrompt: 'Summarize in one concise paragraph for handoff notes.',
|
|
1011
|
-
})
|
|
1012
|
-
if (textResult.success) {
|
|
1013
|
-
// textResult.data: string(摘要文本)
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// 增量摘要(在已有摘要基础上追加)
|
|
1017
|
-
const updated = await ai.summary.summarize(newMessages, {
|
|
1018
|
-
systemPrompt: 'Keep the summary focused on product decisions and next steps.',
|
|
1019
|
-
previousSummary: existingSummary,
|
|
1020
|
-
})
|
|
1021
|
-
```
|
|
1022
|
-
|
|
1023
|
-
---
|
|
1024
|
-
|
|
1025
|
-
## 上下文压缩 — `ai.compress`
|
|
1026
|
-
|
|
1027
|
-
压缩超长对话至指定 Token 预算。
|
|
1028
|
-
|
|
1029
|
-
### 压缩策略
|
|
1030
|
-
|
|
1031
|
-
| 策略 | 说明 |
|
|
1032
|
-
| ---------------- | ---------------------------------------- |
|
|
1033
|
-
| `sliding-window` | 保留 system + 最近 N 条消息,丢弃旧消息 |
|
|
1034
|
-
| `summary` | 对旧消息生成 LLM 摘要替换 |
|
|
1035
|
-
| `hybrid`(默认) | 先滑动窗口,仍超限则对被移除部分生成摘要 |
|
|
1036
|
-
|
|
1037
|
-
### 压缩消息列表
|
|
1038
|
-
|
|
1039
|
-
```ts
|
|
1040
|
-
const result = await ai.compress.tryCompress(messages, {
|
|
1041
|
-
strategy: 'hybrid', // summary | sliding-window | hybrid
|
|
1042
|
-
maxTokens: 4000, // 目标 Token 预算
|
|
1043
|
-
preserveSystem: true, // 保留 system 消息(默认 true)
|
|
1044
|
-
preserveLastN: 4, // 保留最近 N 条不压缩(默认 4)
|
|
1045
|
-
summaryModel: 'gpt-4o-mini', // 摘要用模型
|
|
1046
|
-
})
|
|
1047
|
-
|
|
1048
|
-
if (result.success) {
|
|
1049
|
-
const { messages: compressed, originalTokens, compressedTokens, removedCount, summary } = result.data
|
|
1050
|
-
// compressed: 压缩后的消息列表
|
|
1051
|
-
// originalTokens: 原始 Token 数
|
|
1052
|
-
// compressedTokens: 压缩后 Token 数
|
|
1053
|
-
// removedCount: 被移除/合并的消息数
|
|
1054
|
-
// summary: 生成的摘要文本(仅 summary/hybrid 策略有值)
|
|
1055
|
-
|
|
1056
|
-
const response = await ai.llm.chat({ messages: compressed })
|
|
1057
|
-
}
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
---
|
|
1061
|
-
|
|
1062
|
-
## 上下文管理 — `ai.context`
|
|
1063
|
-
|
|
1064
|
-
有状态上下文管理器,适用于多轮对话场景。自动跟踪 Token 使用量、超限时自动压缩,并可编排 LLM / Memory / RAG / Reasoning / Tools 进行完整的对话流程。支持会话级持久化。
|
|
1065
|
-
|
|
1066
|
-
### 创建管理器并对话
|
|
1067
|
-
|
|
1068
|
-
```ts
|
|
1069
|
-
const managerResult = ai.context.createManager({
|
|
1070
|
-
scope: { objectId: 'user-001', sessionId: 'sess-001' },
|
|
1071
|
-
systemPrompt: '你是一个友好的助手。',
|
|
1072
|
-
model: 'gpt-4o-mini',
|
|
1073
|
-
compress: {
|
|
1074
|
-
maxTokens: 8000,
|
|
1075
|
-
strategy: 'hybrid',
|
|
1076
|
-
preserveLastN: 4,
|
|
1077
|
-
auto: true,
|
|
1078
|
-
},
|
|
1079
|
-
memory: { enable: true, enableExtract: true, topK: 5 },
|
|
1080
|
-
})
|
|
1081
|
-
|
|
1082
|
-
if (!managerResult.success)
|
|
1083
|
-
return
|
|
1084
|
-
|
|
1085
|
-
const manager = managerResult.data
|
|
1086
|
-
|
|
1087
|
-
// 直接对话(自动编排 LLM + 记忆注入/提取 + 压缩)
|
|
1088
|
-
const chatResult = await manager.chat('你好')
|
|
1089
|
-
if (chatResult.success) {
|
|
1090
|
-
// chatResult.data.reply — LLM 回复
|
|
1091
|
-
// chatResult.data.model — 使用的模型
|
|
1092
|
-
// chatResult.data.usage — Token 使用统计
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// 流式对话
|
|
1096
|
-
for await (const event of manager.chatStream('讲个故事')) {
|
|
1097
|
-
if (event.type === 'delta')
|
|
1098
|
-
process.stdout.write(event.text)
|
|
1099
|
-
if (event.type === 'done')
|
|
1100
|
-
console.warn('\n完成', event.model)
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// 持久化
|
|
1104
|
-
await manager.save()
|
|
1105
|
-
```
|
|
1106
|
-
|
|
1107
|
-
### 底层消息管理
|
|
1108
|
-
|
|
1109
|
-
```ts
|
|
1110
|
-
const managerResult = ai.context.createManager({
|
|
1111
|
-
compress: { maxTokens: 8000, strategy: 'hybrid' },
|
|
1112
|
-
})
|
|
1113
|
-
const manager = managerResult.data
|
|
1114
|
-
|
|
1115
|
-
// 手动追加消息(超限时自动触发压缩)
|
|
1116
|
-
await manager.addMessage({ role: 'user', content: userInput })
|
|
1117
|
-
|
|
1118
|
-
// 获取当前消息列表(已压缩)
|
|
1119
|
-
const msgs = manager.getMessages()
|
|
1120
|
-
const response = await ai.llm.chat({ messages: msgs.data })
|
|
1121
|
-
|
|
1122
|
-
// 追加助手回复
|
|
1123
|
-
await manager.addMessage(response.data.choices[0].message)
|
|
1124
|
-
|
|
1125
|
-
// 查看 Token 使用量
|
|
1126
|
-
const usage = manager.getTokenUsage()
|
|
1127
|
-
// usage.data.current / usage.data.budget
|
|
1128
|
-
|
|
1129
|
-
// 查看历史摘要
|
|
1130
|
-
const summaries = manager.getSummaries()
|
|
1131
|
-
|
|
1132
|
-
// 重置
|
|
1133
|
-
manager.reset()
|
|
1134
|
-
```
|
|
1135
|
-
|
|
1136
|
-
### 恢复已有会话
|
|
1137
|
-
|
|
1138
|
-
```ts
|
|
1139
|
-
const managerResult = await ai.context.restoreManager(
|
|
1140
|
-
{ objectId: 'user-001', sessionId: 'sess-001' },
|
|
1141
|
-
{ compress: { maxTokens: 8000 }, memory: { enable: true } },
|
|
1142
|
-
)
|
|
1143
|
-
if (managerResult.success) {
|
|
1144
|
-
const manager = managerResult.data
|
|
1145
|
-
// manager 已包含之前保存的消息和摘要
|
|
1146
|
-
const result = await manager.chat('继续上次的话题')
|
|
1147
|
-
}
|
|
1148
|
-
```
|
|
1149
|
-
|
|
1150
|
-
### 启用 RAG / Reasoning / Tools
|
|
1151
|
-
|
|
1152
|
-
```ts
|
|
1153
|
-
const registry = ai.tools.createRegistry()
|
|
1154
|
-
registry.register(ai.tools.define({
|
|
1155
|
-
name: 'search',
|
|
1156
|
-
description: '搜索',
|
|
1157
|
-
parameters: z.object({ query: z.string() }),
|
|
1158
|
-
execute: async ({ query }) => `结果: ${query}`,
|
|
1159
|
-
}))
|
|
1160
|
-
|
|
1161
|
-
const managerResult = ai.context.createManager({
|
|
1162
|
-
scope: { objectId: 'user-001', sessionId: 'sess-001' },
|
|
1163
|
-
rag: { enable: true, sources: ['docs'], topK: 5 },
|
|
1164
|
-
reasoning: { enable: true, strategy: 'react', maxRounds: 5 },
|
|
1165
|
-
tools: registry,
|
|
1166
|
-
})
|
|
1167
|
-
```
|
|
1168
|
-
|
|
1169
|
-
### 列出对象的所有会话
|
|
1170
|
-
|
|
1171
|
-
```ts
|
|
1172
|
-
const result = await ai.context.listSessions('user-001')
|
|
1173
|
-
if (result.success) {
|
|
1174
|
-
for (const session of result.data) {
|
|
1175
|
-
// session.sessionId / session.objectId / session.createdAt / session.updatedAt
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
```
|
|
1179
|
-
|
|
1180
|
-
---
|
|
1181
|
-
|
|
1182
|
-
## 重排序 — `ai.rerank`
|
|
1183
|
-
|
|
1184
|
-
对检索结果按查询相关性重新排序,提升 RAG / 检索质量。
|
|
1185
|
-
|
|
1186
|
-
```ts
|
|
1187
|
-
// 快捷方式:文本列表重排序
|
|
1188
|
-
const result = await ai.rerank.rerankTexts('数据库配置方法', [
|
|
1189
|
-
'本文介绍如何配置 PostgreSQL 连接池',
|
|
1190
|
-
'前端组件的生命周期管理',
|
|
1191
|
-
'数据库连接字符串的最佳实践',
|
|
1192
|
-
], 2) // topN: 返回前 2 个最相关的
|
|
1193
|
-
|
|
1194
|
-
if (result.success) {
|
|
1195
|
-
for (const item of result.data) {
|
|
1196
|
-
// item.index — 原始位置
|
|
1197
|
-
// item.relevanceScore — 相关性分数
|
|
1198
|
-
// item.document — 原始文本
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
// 完整请求
|
|
1203
|
-
const result = await ai.rerank.rerank({
|
|
1204
|
-
query: '配置方法',
|
|
1205
|
-
documents: ['文档1', '文档2', '文档3'],
|
|
1206
|
-
topN: 3,
|
|
1207
|
-
returnDocuments: true,
|
|
1208
|
-
})
|
|
1209
|
-
```
|
|
1210
|
-
|
|
1211
|
-
---
|
|
1212
|
-
|
|
1213
|
-
## 文件解析 — `ai.file`
|
|
1214
|
-
|
|
1215
|
-
解析多种格式文件为文本内容,支持 OCR。
|
|
1216
|
-
|
|
1217
|
-
```ts
|
|
1218
|
-
const result = await ai.file.parse({
|
|
1219
|
-
content: fileBuffer, // Buffer | string
|
|
1220
|
-
mimeType: 'application/pdf',
|
|
1221
|
-
maxPages: 50,
|
|
1222
|
-
outputFormat: 'markdown', // 'text' | 'markdown'
|
|
1223
|
-
useOcr: false, // 使用 Vision LLM 做 OCR
|
|
1224
|
-
})
|
|
1225
|
-
|
|
1226
|
-
if (result.success) {
|
|
1227
|
-
// result.data.text — 解析出的文本内容
|
|
1228
|
-
// result.data.pages — 页数(PDF 有效)
|
|
1229
|
-
// result.data.metadata — 文件元信息
|
|
1230
|
-
}
|
|
1231
|
-
```
|
|
1232
|
-
|
|
1233
|
-
**支持格式**:`text`(UTF-8)、`html`(HTML→text)、`pdf`(pdfjs-dist)、`docx`(mammoth)、`ocr`(Vision LLM)。
|
|
1234
|
-
|
|
1235
|
-
---
|
|
1236
|
-
|
|
1237
|
-
## A2A — `ai.a2a`
|
|
1238
|
-
|
|
1239
|
-
Agent-to-Agent 协议支持,实现代理间通信与任务委派。A2A 为延迟初始化子系统,需在配置中提供 `a2a` 配置段。
|
|
1240
|
-
|
|
1241
|
-
### 配置
|
|
1242
|
-
|
|
1243
|
-
```ts
|
|
1244
|
-
await ai.init({
|
|
1245
|
-
llm: { model: 'gpt-4o-mini', apiKey: process.env.API_KEY },
|
|
1246
|
-
a2a: {
|
|
1247
|
-
agentCard: {
|
|
1248
|
-
name: 'my-agent',
|
|
1249
|
-
description: '智能助手',
|
|
1250
|
-
url: 'https://agent.example.com',
|
|
1251
|
-
skills: [{ id: 'chat', name: '对话', description: '通用对话能力' }],
|
|
1252
|
-
},
|
|
1253
|
-
},
|
|
1254
|
-
})
|
|
1255
|
-
```
|
|
1256
|
-
|
|
1257
|
-
### 注册执行器并处理请求
|
|
1258
|
-
|
|
1259
|
-
```ts
|
|
1260
|
-
import type { AgentExecutor } from '@h-ai/ai'
|
|
1261
|
-
|
|
1262
|
-
// 实现执行器
|
|
1263
|
-
const executor: AgentExecutor = {
|
|
1264
|
-
async* execute(context) {
|
|
1265
|
-
// context.userMessage — 用户消息
|
|
1266
|
-
// 处理任务并返回结果
|
|
1267
|
-
yield { state: 'completed', message: { role: 'agent', parts: [{ type: 'text', text: '处理完成' }] } }
|
|
1268
|
-
},
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
ai.a2a.registerExecutor(executor)
|
|
1272
|
-
|
|
1273
|
-
// 在 API 端点中处理请求
|
|
1274
|
-
const result = await ai.a2a.handleRequest(requestBody)
|
|
1275
|
-
if (result.streaming) {
|
|
1276
|
-
// result.stream — AsyncGenerator(流式响应)
|
|
1277
|
-
}
|
|
1278
|
-
else {
|
|
1279
|
-
// result.body — JSON 响应
|
|
1280
|
-
}
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
### 调用远端代理
|
|
1284
|
-
|
|
1285
|
-
```ts
|
|
1286
|
-
const result = await ai.a2a.callRemoteAgent(
|
|
1287
|
-
'https://remote-agent.example.com',
|
|
1288
|
-
'帮我分析这份季度报告',
|
|
1289
|
-
{ timeout: 30000 },
|
|
1290
|
-
)
|
|
1291
|
-
|
|
1292
|
-
if (result.success) {
|
|
1293
|
-
// result.data.taskId — 任务 ID
|
|
1294
|
-
// result.data.taskState — 任务状态
|
|
1295
|
-
// result.data.responseText — 回复文本
|
|
1296
|
-
}
|
|
1297
|
-
```
|
|
1298
|
-
|
|
1299
|
-
### 查询消息记录
|
|
1300
|
-
|
|
1301
|
-
```ts
|
|
1302
|
-
const messages = await ai.a2a.listMessages({
|
|
1303
|
-
contextId: 'ctx-001',
|
|
1304
|
-
limit: 20,
|
|
1305
|
-
})
|
|
1306
|
-
if (messages.success) {
|
|
1307
|
-
// messages.data.items: A2AMessageRecord[]
|
|
1308
|
-
// messages.data.total: number
|
|
1309
|
-
}
|
|
1310
|
-
```
|
|
1311
|
-
|
|
1312
|
-
---
|
|
1313
|
-
|
|
1314
|
-
## MCP 服务器 — `createMcpServer`
|
|
1315
|
-
|
|
1316
|
-
> 独立工厂函数,**不需要初始化**即可使用。
|
|
1317
|
-
|
|
1318
|
-
### 创建服务器
|
|
1319
|
-
|
|
1320
|
-
```ts
|
|
1321
|
-
import { createMcpServer, StreamableHTTPServerTransport } from '@h-ai/ai'
|
|
1322
|
-
import { z } from 'zod'
|
|
1323
|
-
|
|
1324
|
-
const mcp = createMcpServer({ name: 'my-server', version: '1.0.0' })
|
|
1325
|
-
```
|
|
1326
|
-
|
|
1327
|
-
### 注册工具
|
|
1328
|
-
|
|
1329
|
-
```ts
|
|
1330
|
-
mcp.registerTool('search', {
|
|
1331
|
-
description: '搜索文档',
|
|
1332
|
-
inputSchema: { query: z.string(), limit: z.number().optional() },
|
|
1333
|
-
}, async ({ query, limit }) => ({
|
|
1334
|
-
content: [{ type: 'text', text: `Found ${limit ?? 10} results for: ${query}` }],
|
|
1335
|
-
}))
|
|
1336
|
-
```
|
|
1337
|
-
|
|
1338
|
-
### 注册资源
|
|
1339
|
-
|
|
1340
|
-
```ts
|
|
1341
|
-
mcp.registerResource('config', 'config://app', {
|
|
1342
|
-
description: '应用配置',
|
|
1343
|
-
}, async uri => ({
|
|
1344
|
-
contents: [{ uri: uri.href, text: JSON.stringify(appConfig) }],
|
|
1345
|
-
}))
|
|
1346
|
-
```
|
|
1347
|
-
|
|
1348
|
-
### 注册提示词
|
|
1349
|
-
|
|
1350
|
-
```ts
|
|
1351
|
-
mcp.registerPrompt('summarize', {
|
|
1352
|
-
description: '总结文本',
|
|
1353
|
-
argsSchema: { text: z.string() },
|
|
1354
|
-
}, async ({ text }) => ({
|
|
1355
|
-
messages: [{ role: 'user', content: { type: 'text', text: `请总结:${text}` } }],
|
|
1356
|
-
}))
|
|
1357
|
-
```
|
|
1358
|
-
|
|
1359
|
-
### 连接传输层
|
|
1360
|
-
|
|
1361
|
-
```ts
|
|
1362
|
-
import { randomUUID } from 'node:crypto'
|
|
1363
|
-
|
|
1364
|
-
// Streamable HTTP(推荐)
|
|
1365
|
-
const transport = new StreamableHTTPServerTransport({
|
|
1366
|
-
sessionIdGenerator: () => randomUUID(),
|
|
1367
|
-
})
|
|
1368
|
-
await mcp.connect(transport)
|
|
1369
|
-
|
|
1370
|
-
// 其他传输层
|
|
1371
|
-
// import { SSEServerTransport, StdioServerTransport } from '@h-ai/ai'
|
|
1372
|
-
```
|
|
1373
|
-
|
|
1374
|
-
---
|
|
1375
|
-
|
|
1376
|
-
## MCP 操作 — `ai.mcp`
|
|
1377
|
-
|
|
1378
|
-
作为 MCP 客户端注册与调用工具、资源和提示词。
|
|
1379
|
-
|
|
1380
|
-
```ts
|
|
1381
|
-
// 注册工具
|
|
1382
|
-
ai.mcp.registerTool(
|
|
1383
|
-
{ name: 'calc', description: '计算', inputSchema: { expr: { type: 'string' } } },
|
|
1384
|
-
async ({ expr }) => ({ result: '2' }),
|
|
1385
|
-
)
|
|
1386
|
-
|
|
1387
|
-
// 调用工具
|
|
1388
|
-
const result = await ai.mcp.callTool('calc', { expr: '1+1' })
|
|
1389
|
-
|
|
1390
|
-
// 注册并读取资源
|
|
1391
|
-
ai.mcp.registerResource(
|
|
1392
|
-
{ uri: 'data://users', name: 'users', description: '用户列表' },
|
|
1393
|
-
async () => ({ uri: 'data://users', text: JSON.stringify(users) }),
|
|
1394
|
-
)
|
|
1395
|
-
const resource = await ai.mcp.readResource('data://users')
|
|
1396
|
-
|
|
1397
|
-
// 注册并获取提示词
|
|
1398
|
-
ai.mcp.registerPrompt(
|
|
1399
|
-
{ name: 'greet', arguments: [{ name: 'name', required: true }] },
|
|
1400
|
-
async ({ name }) => [{ role: 'user', content: { type: 'text', text: `你好 ${name}` } }],
|
|
1401
|
-
)
|
|
1402
|
-
const prompt = await ai.mcp.getPrompt('greet', { name: '张三' })
|
|
1403
|
-
```
|
|
1404
|
-
|
|
1405
|
-
---
|
|
1406
|
-
|
|
1407
|
-
## 前端客户端 — `createAIClient`
|
|
1408
|
-
|
|
1409
|
-
> 浏览器端轻量客户端,通过 API 代理层与后端 AI 通信。
|
|
1410
|
-
|
|
1411
|
-
```ts
|
|
1412
|
-
import { createAIClient } from '@h-ai/ai/client'
|
|
1413
|
-
import { api } from '@h-ai/api-client'
|
|
1414
|
-
|
|
1415
|
-
await api.init({ baseUrl: '/api' })
|
|
1416
|
-
const client = createAIClient({ api })
|
|
1417
|
-
```
|
|
1418
|
-
|
|
1419
|
-
### 非流式对话
|
|
1420
|
-
|
|
1421
|
-
```ts
|
|
1422
|
-
const response = await client.chat({
|
|
1423
|
-
messages: [{ role: 'user', content: '你好' }],
|
|
1424
|
-
})
|
|
1425
|
-
// response: ChatCompletionResponse(同服务端返回结构)
|
|
1426
|
-
```
|
|
1427
|
-
|
|
1428
|
-
### 流式对话
|
|
1429
|
-
|
|
1430
|
-
```ts
|
|
1431
|
-
for await (const chunk of client.chatStream({ messages }, {
|
|
1432
|
-
onProgress: (progress) => {
|
|
1433
|
-
// progress.content — 已累积的内容
|
|
1434
|
-
// progress.done — 是否结束
|
|
1435
|
-
// progress.finishReason — 结束原因
|
|
1436
|
-
},
|
|
1437
|
-
})) {
|
|
1438
|
-
const delta = chunk.choices[0]?.delta?.content
|
|
1439
|
-
if (delta) {
|
|
1440
|
-
updateUI(delta)
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
```
|
|
1444
|
-
|
|
1445
|
-
### 便捷方式
|
|
1446
|
-
|
|
1447
|
-
```ts
|
|
1448
|
-
// 发送单条消息,返回回复文本
|
|
1449
|
-
const reply = await client.sendMessage('你好', '你是一个翻译助手')
|
|
1450
|
-
|
|
1451
|
-
// 流式发送,返回完整回复文本
|
|
1452
|
-
const reply = await client.sendMessageStream('写一首诗', {
|
|
1453
|
-
onProgress: p => updateUI(p.content),
|
|
1454
|
-
})
|
|
1455
|
-
```
|
|
1456
|
-
|
|
1457
|
-
### 记忆与会话查询
|
|
1458
|
-
|
|
1459
|
-
```ts
|
|
1460
|
-
// 检索相关记忆
|
|
1461
|
-
const memories = await client.recallMemories('用户偏好', {
|
|
1462
|
-
topK: 5,
|
|
1463
|
-
objectId: 'user-001',
|
|
1464
|
-
})
|
|
1465
|
-
|
|
1466
|
-
// 分页列出记忆
|
|
1467
|
-
const page = await client.listMemories({
|
|
1468
|
-
objectId: 'user-001',
|
|
1469
|
-
offset: 0,
|
|
1470
|
-
limit: 20,
|
|
1471
|
-
})
|
|
1472
|
-
// page.items: MemoryEntry[], page.total: number
|
|
1473
|
-
|
|
1474
|
-
// 列出某对象的所有会话
|
|
1475
|
-
const sessions = await client.listSessions('user-001')
|
|
1476
|
-
// sessions: SessionInfo[]
|
|
1477
|
-
```
|
|
1478
|
-
|
|
1479
|
-
---
|
|
1480
|
-
|
|
1481
|
-
## 错误处理
|
|
1482
|
-
|
|
1483
|
-
所有需要初始化的子系统方法均返回 `HaiResult<T>`,通过 `result.success` 判断成功/失败。
|
|
1484
|
-
|
|
1485
|
-
```ts
|
|
1486
|
-
import { HaiAIError } from '@h-ai/ai'
|
|
1487
|
-
|
|
1488
|
-
const result = await ai.llm.chat({ messages })
|
|
1489
|
-
|
|
1490
|
-
if (!result.success) {
|
|
1491
|
-
switch (result.error.code) {
|
|
1492
|
-
case HaiAIError.NOT_INITIALIZED.code:
|
|
1493
|
-
// 未初始化,请先调用 ai.init()
|
|
1494
|
-
break
|
|
1495
|
-
case HaiAIError.RATE_LIMITED.code:
|
|
1496
|
-
// API 限流,稍后重试
|
|
1497
|
-
break
|
|
1498
|
-
case HaiAIError.TIMEOUT.code:
|
|
1499
|
-
// 请求超时
|
|
1500
|
-
break
|
|
1501
|
-
case HaiAIError.CONTEXT_LENGTH_EXCEEDED.code:
|
|
1502
|
-
// 上下文超长,需压缩消息
|
|
1503
|
-
break
|
|
1504
|
-
case HaiAIError.MEMORY_RECALL_FAILED.code:
|
|
1505
|
-
// 记忆检索失败
|
|
1506
|
-
break
|
|
1507
|
-
case HaiAIError.CONTEXT_COMPRESS_FAILED.code:
|
|
1508
|
-
// 上下文压缩失败
|
|
1509
|
-
break
|
|
1510
|
-
default:
|
|
1511
|
-
// 通用错误
|
|
1512
|
-
break
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
```
|
|
1516
|
-
|
|
1517
|
-
---
|
|
1518
|
-
|
|
1519
|
-
## 设计说明
|
|
1520
|
-
|
|
1521
|
-
### 架构概览
|
|
1522
|
-
|
|
1523
|
-
AI 是**生命周期单例**模块,通过 `ai.init()` / `ai.close()` 管理运行时状态。内部由 17 个子模块构成,通过 getter 暴露给调用方。
|
|
1524
|
-
|
|
1525
|
-
```
|
|
1526
|
-
ai.init(config?, options?)
|
|
1527
|
-
├─ 配置校验(Zod schema)
|
|
1528
|
-
├─ 存储 Provider 解析
|
|
1529
|
-
│ ├─ options.storeProvider → 使用自定义 Provider
|
|
1530
|
-
│ └─ 未提供 → 检查 reldb + vecdb 已初始化 → 创建默认 DB Provider
|
|
1531
|
-
├─ createAISubsystems() — 按依赖创建所有子模块
|
|
1532
|
-
│ ├─ LLM Provider(OpenAI 兼容,多模型场景映射)
|
|
1533
|
-
│ ├─ MCP(工具/资源/提示词注册表)
|
|
1534
|
-
│ ├─ Embedding(文本向量化)
|
|
1535
|
-
│ ├─ Token(Token 估算)
|
|
1536
|
-
│ ├─ Summary(消息摘要)
|
|
1537
|
-
│ ├─ Compress(上下文压缩)
|
|
1538
|
-
│ ├─ Memory(记忆提取/存储/检索)
|
|
1539
|
-
│ ├─ Rerank(结果重排序)
|
|
1540
|
-
│ ├─ Retrieval(多源向量检索)
|
|
1541
|
-
│ ├─ RAG(检索增强问答)
|
|
1542
|
-
│ ├─ Knowledge(知识库管理)
|
|
1543
|
-
│ ├─ Reasoning(推理引擎)
|
|
1544
|
-
│ ├─ Context(有状态上下文管理器)
|
|
1545
|
-
│ ├─ File(文件解析)
|
|
1546
|
-
│ └─ A2A(Agent-to-Agent,延迟初始化)
|
|
1547
|
-
└─ StoreProvider.setupAll()(建表/建连接)
|
|
1548
|
-
```
|
|
1549
|
-
|
|
1550
|
-
初始化前访问任何需要 init 的子模块方法,均返回 `NOT_INITIALIZED` 错误(基于 `NotInitializedKit` Proxy)。`ai.tools` 和 `ai.stream` 为纯函数,**无需初始化**。
|
|
1551
|
-
|
|
1552
|
-
### 数据存储分布
|
|
1553
|
-
|
|
1554
|
-
> 下表为默认 DB Provider(reldb + vecdb)的存储映射。自定义 `AIStoreProvider` 可对接其他存储后端。
|
|
1555
|
-
|
|
1556
|
-
| 数据类型 | 存储位置 | 表/集合名 |
|
|
1557
|
-
| ---------- | ---------- | ----------------------------------------- |
|
|
1558
|
-
| 对话记录 | DB | `hai_ai_chat_records` |
|
|
1559
|
-
| 会话元数据 | DB | `hai_ai_sessions` |
|
|
1560
|
-
| 检索源配置 | DB | `hai_ai_retrieval_sources` |
|
|
1561
|
-
| 记忆条目 | DB + VecDB | `hai_ai_memory` + `hai_ai_memory_vectors` |
|
|
1562
|
-
| 上下文状态 | DB | `hai_ai_context` |
|
|
1563
|
-
| 知识库内容 | VecDB | `hai_ai_knowledge`(可配置) |
|
|
1564
|
-
|
|
1565
|
-
### 多模型场景映射
|
|
1566
|
-
|
|
1567
|
-
通过 `llm.models` 注册多个模型,通过 `llm.scenarios` 将不同使用场景映射到对应模型:
|
|
1568
|
-
|
|
1569
|
-
```ts
|
|
1570
|
-
await ai.init({
|
|
1571
|
-
llm: {
|
|
1572
|
-
model: 'gpt-4o-mini', // 默认模型
|
|
1573
|
-
models: [
|
|
1574
|
-
{ id: 'fast', model: 'gpt-4o-mini', temperature: 0.3 },
|
|
1575
|
-
{ id: 'strong', model: 'gpt-4o', maxTokens: 8192 },
|
|
1576
|
-
{ id: 'embed', model: 'text-embedding-3-small' },
|
|
1577
|
-
],
|
|
1578
|
-
scenarios: {
|
|
1579
|
-
default: 'fast',
|
|
1580
|
-
chat: 'fast',
|
|
1581
|
-
reasoning: 'strong',
|
|
1582
|
-
embedding: 'embed',
|
|
1583
|
-
summary: 'fast',
|
|
1584
|
-
extraction: 'fast',
|
|
1585
|
-
rerank: 'fast',
|
|
1586
|
-
ocr: 'strong',
|
|
1587
|
-
},
|
|
1588
|
-
},
|
|
1589
|
-
})
|
|
1590
|
-
```
|
|
1591
|
-
|
|
1592
|
-
各子模块自动使用对应场景的模型:
|
|
1593
|
-
|
|
1594
|
-
- `ai.llm.chat()`:`scenarios.chat` → `scenarios.default` → `llm.model`
|
|
1595
|
-
- `ai.embedding.embed()`:`scenarios.embedding` → `llm.model`
|
|
1596
|
-
- `ai.reasoning.run()`:`scenarios.reasoning` → `llm.model`
|
|
1597
|
-
- `ai.summary.summarize()`:`scenarios.summary` → `llm.model`
|
|
1598
|
-
- `ai.rerank.rerank()`:`scenarios.rerank` → `llm.model`
|
|
1599
|
-
|
|
1600
|
-
### Provider 模式
|
|
1601
|
-
|
|
1602
|
-
#### LLM Provider
|
|
1603
|
-
|
|
1604
|
-
当前内置 **OpenAI 兼容** Provider(`ai-llm-provider-openai.ts`),支持所有 OpenAI API 兼容服务(OpenAI、Azure OpenAI、Ollama、vLLM 等):
|
|
1605
|
-
|
|
1606
|
-
- API Key 解析顺序:模型条目 > 全局配置 > 环境变量(`HAI_AI_LLM_API_KEY` / `OPENAI_API_KEY`)
|
|
1607
|
-
- Base URL 解析顺序:模型条目 > 全局配置 > 环境变量(`HAI_AI_LLM_BASE_URL` / `OPENAI_BASE_URL`)> OpenAI 默认
|
|
1608
|
-
|
|
1609
|
-
#### Store Provider
|
|
1610
|
-
|
|
1611
|
-
存储层通过 `AIStoreProvider` 接口抽象,所有需要持久化的子系统(Memory / Knowledge / Context / Retrieval / A2A 等)统一通过此接口存取数据:
|
|
1612
|
-
|
|
1613
|
-
```ts
|
|
1614
|
-
interface AIStoreProvider {
|
|
1615
|
-
readonly name: string
|
|
1616
|
-
createRelStore: <T>(name: string, options?: AIRelStoreOptions) => AIRelStore<T>
|
|
1617
|
-
createVectorStore: (name: string) => AIVectorStore
|
|
1618
|
-
createKnowledgeStore?: () => KnowledgeStore
|
|
1619
|
-
setupAll: () => Promise<void>
|
|
1620
|
-
}
|
|
1621
|
-
```
|
|
1622
|
-
|
|
1623
|
-
- **默认 DB Provider**:基于 `@h-ai/reldb` + `@h-ai/vecdb`,未传入自定义 Provider 时自动使用
|
|
1624
|
-
- **自定义 Provider**:通过 `ai.init(config, { storeProvider })` 注入,可对接 SaaS API、其他数据库等
|
|
1625
|
-
|
|
1626
|
-
### 上下文管理编排
|
|
1627
|
-
|
|
1628
|
-
`ai.context.createManager()` 创建的 ContextManager 是最上层的编排器,一次 `chat()` 调用的内部流程:
|
|
1629
|
-
|
|
1630
|
-
```
|
|
1631
|
-
manager.chat(query)
|
|
1632
|
-
├─ 记忆注入(memory.injectMemories → 从 VecDB 召回相关记忆追加到 system)
|
|
1633
|
-
├─ RAG 检索(retrieval.retrieve → 从知识库检索相关片段注入上下文)
|
|
1634
|
-
├─ 上下文压缩(compress.tryCompress → 超限时自动压缩旧消息)
|
|
1635
|
-
├─ 推理引擎(reasoning.run → CoT/ReAct/Plan-Execute)或 LLM 直接调用
|
|
1636
|
-
├─ 工具调用循环(tools 注册表 → 自动执行并续接)
|
|
1637
|
-
├─ 记忆提取(memory.extract → 从新对话中提取事实/偏好/实体等)
|
|
1638
|
-
└─ 持久化(save → 消息和摘要写入 DB)
|
|
1639
|
-
```
|
|
1640
|
-
|
|
1641
|
-
---
|
|
1642
|
-
|
|
1643
|
-
## 典型场景
|
|
1644
|
-
|
|
1645
|
-
### 带工具调用的多轮对话循环
|
|
1646
|
-
|
|
1647
|
-
```ts
|
|
1648
|
-
import type { ChatMessage } from '@h-ai/ai'
|
|
1649
|
-
import { ai } from '@h-ai/ai'
|
|
1650
|
-
import { z } from 'zod'
|
|
1651
|
-
|
|
1652
|
-
// 定义并注册工具
|
|
1653
|
-
const weatherTool = ai.tools.define({
|
|
1654
|
-
name: 'get_weather',
|
|
1655
|
-
description: '获取天气',
|
|
1656
|
-
parameters: z.object({ city: z.string() }),
|
|
1657
|
-
handler: async ({ city }) => ({ temp: 22, city, condition: '晴' }),
|
|
1658
|
-
})
|
|
1659
|
-
|
|
1660
|
-
const registry = ai.tools.createRegistry()
|
|
1661
|
-
registry.register(weatherTool)
|
|
1662
|
-
|
|
1663
|
-
const messages: ChatMessage[] = [
|
|
1664
|
-
{ role: 'user', content: '北京和上海今天天气怎么样?' },
|
|
1665
|
-
]
|
|
1666
|
-
|
|
1667
|
-
// 对话循环
|
|
1668
|
-
let result = await ai.llm.chat({
|
|
1669
|
-
messages,
|
|
1670
|
-
tools: registry.getDefinitions(),
|
|
1671
|
-
})
|
|
1672
|
-
|
|
1673
|
-
while (result.success && result.data.choices[0].finish_reason === 'tool_calls') {
|
|
1674
|
-
const assistantMsg = result.data.choices[0].message
|
|
1675
|
-
messages.push(assistantMsg)
|
|
1676
|
-
|
|
1677
|
-
const toolResults = await registry.executeAll(assistantMsg.tool_calls!, { parallel: true })
|
|
1678
|
-
if (toolResults.success) {
|
|
1679
|
-
messages.push(...toolResults.data)
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
result = await ai.llm.chat({ messages, tools: registry.getDefinitions() })
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
if (result.success) {
|
|
1686
|
-
const finalAnswer = result.data.choices[0].message.content
|
|
1687
|
-
}
|
|
1688
|
-
```
|
|
1689
|
-
|
|
1690
|
-
### 记忆增强的长期对话
|
|
1691
|
-
|
|
1692
|
-
```ts
|
|
1693
|
-
import type { ChatMessage } from '@h-ai/ai'
|
|
1694
|
-
import { ai } from '@h-ai/ai'
|
|
1695
|
-
|
|
1696
|
-
await ai.init({
|
|
1697
|
-
llm: { model: 'gpt-4o-mini', apiKey: process.env.API_KEY },
|
|
1698
|
-
memory: { maxEntries: 500, embeddingEnabled: true },
|
|
1699
|
-
compress: { defaultStrategy: 'hybrid', preserveLastN: 6 },
|
|
1700
|
-
})
|
|
1701
|
-
|
|
1702
|
-
const messages: ChatMessage[] = [
|
|
1703
|
-
{ role: 'system', content: '你是用户的私人助手,请记住用户的偏好。' },
|
|
1704
|
-
]
|
|
1705
|
-
|
|
1706
|
-
async function chat(userInput: string): Promise<string | undefined> {
|
|
1707
|
-
messages.push({ role: 'user', content: userInput })
|
|
1708
|
-
|
|
1709
|
-
// 1. 注入历史记忆
|
|
1710
|
-
const enriched = await ai.memory.injectMemories(messages, {
|
|
1711
|
-
topK: 5,
|
|
1712
|
-
position: 'system',
|
|
1713
|
-
maxTokens: 500,
|
|
1714
|
-
})
|
|
1715
|
-
const enrichedMessages = enriched.success ? enriched.data : messages
|
|
1716
|
-
|
|
1717
|
-
// 2. 压缩上下文以适应模型限制
|
|
1718
|
-
const compressed = await ai.compress.tryCompress(enrichedMessages, {
|
|
1719
|
-
maxTokens: 4000,
|
|
1720
|
-
strategy: 'hybrid',
|
|
1721
|
-
})
|
|
1722
|
-
const finalMessages = compressed.success ? compressed.data.messages : enrichedMessages
|
|
1723
|
-
|
|
1724
|
-
// 3. 调用 LLM
|
|
1725
|
-
const result = await ai.llm.chat({ messages: finalMessages })
|
|
1726
|
-
if (!result.success)
|
|
1727
|
-
return undefined
|
|
1728
|
-
|
|
1729
|
-
const reply = result.data.choices[0].message
|
|
1730
|
-
messages.push(reply)
|
|
1731
|
-
|
|
1732
|
-
// 4. 异步提取新记忆(不阻塞响应)
|
|
1733
|
-
ai.memory.extract(messages.slice(-2), { objectId: 'chat' })
|
|
1734
|
-
|
|
1735
|
-
return reply.content ?? undefined
|
|
1736
|
-
}
|
|
1737
|
-
```
|
|
1738
|
-
|
|
1739
|
-
### 知识库 + RAG 完整流程
|
|
1740
|
-
|
|
1741
|
-
```ts
|
|
1742
|
-
import { ai } from '@h-ai/ai'
|
|
1743
|
-
|
|
1744
|
-
// 初始化(默认 DB Provider 需要 vecdb + reldb)
|
|
1745
|
-
await ai.init({
|
|
1746
|
-
llm: { model: 'gpt-4o-mini', apiKey: process.env.API_KEY },
|
|
1747
|
-
embedding: { model: 'text-embedding-3-small' },
|
|
1748
|
-
knowledge: {
|
|
1749
|
-
enableEntityExtraction: true,
|
|
1750
|
-
cleanOptions: { removeHtml: true },
|
|
1751
|
-
chunkOptions: { mode: 'markdown', maxSize: 1500, overlap: 200 },
|
|
1752
|
-
},
|
|
1753
|
-
})
|
|
1754
|
-
|
|
1755
|
-
// 1. 初始化知识库
|
|
1756
|
-
await ai.knowledge.setup()
|
|
1757
|
-
|
|
1758
|
-
// 2. 批量导入文档
|
|
1759
|
-
const documents = [
|
|
1760
|
-
{ id: 'doc-1', content: '# 产品架构\n...', title: '架构文档' },
|
|
1761
|
-
{ id: 'doc-2', content: '# 部署指南\n...', title: '部署文档' },
|
|
1762
|
-
]
|
|
1763
|
-
|
|
1764
|
-
for (const doc of documents) {
|
|
1765
|
-
const result = await ai.knowledge.ingest({
|
|
1766
|
-
documentId: doc.id,
|
|
1767
|
-
content: doc.content,
|
|
1768
|
-
title: doc.title,
|
|
1769
|
-
enableEntityExtraction: true,
|
|
1770
|
-
})
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
// 3. 知识问答
|
|
1774
|
-
const answer = await ai.knowledge.ask('项目的部署流程是什么?', {
|
|
1775
|
-
topK: 5,
|
|
1776
|
-
enableEntityBoost: true,
|
|
1777
|
-
systemPrompt: '基于文档准确回答,标注引用来源。',
|
|
1778
|
-
})
|
|
1779
|
-
|
|
1780
|
-
if (answer.success) {
|
|
1781
|
-
// answer.data.answer — LLM 回答
|
|
1782
|
-
// answer.data.citations — 引用列表
|
|
1783
|
-
}
|
|
1784
|
-
```
|
|
1785
|
-
|
|
1786
|
-
### 有状态上下文管理器 + 流式输出
|
|
1787
|
-
|
|
1788
|
-
```ts
|
|
1789
|
-
import { ai } from '@h-ai/ai'
|
|
1790
|
-
|
|
1791
|
-
await ai.init({
|
|
1792
|
-
llm: { model: 'gpt-4o-mini', apiKey: process.env.API_KEY },
|
|
1793
|
-
compress: { defaultStrategy: 'hybrid', preserveLastN: 6 },
|
|
1794
|
-
})
|
|
1795
|
-
|
|
1796
|
-
// 创建管理器(使用嵌套 compress 配置)
|
|
1797
|
-
const managerResult = ai.context.createManager({
|
|
1798
|
-
compress: { maxTokens: 8000, auto: true },
|
|
1799
|
-
memory: { enable: true, enableExtract: true },
|
|
1800
|
-
})
|
|
1801
|
-
if (!managerResult.success)
|
|
1802
|
-
return
|
|
1803
|
-
const manager = managerResult.data
|
|
1804
|
-
|
|
1805
|
-
// 方式一:直接使用 chatStream(推荐,自动编排)
|
|
1806
|
-
for await (const event of manager.chatStream('你好')) {
|
|
1807
|
-
if (event.type === 'delta')
|
|
1808
|
-
process.stdout.write(event.text)
|
|
1809
|
-
if (event.type === 'done')
|
|
1810
|
-
console.warn('\n完成,模型:', event.model)
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
// 方式二:手动编排
|
|
1814
|
-
async function streamChat(userInput: string): Promise<string> {
|
|
1815
|
-
await manager.addMessage({ role: 'user', content: userInput })
|
|
1816
|
-
|
|
1817
|
-
const msgs = manager.getMessages()
|
|
1818
|
-
if (!msgs.success)
|
|
1819
|
-
return ''
|
|
1820
|
-
|
|
1821
|
-
const processor = ai.stream.createProcessor()
|
|
1822
|
-
for await (const chunk of ai.llm.chatStream({ messages: msgs.data })) {
|
|
1823
|
-
const delta = processor.process(chunk)
|
|
1824
|
-
if (delta?.content) {
|
|
1825
|
-
process.stdout.write(delta.content)
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
const assistantMsg = processor.toAssistantMessage()
|
|
1830
|
-
await manager.addMessage(assistantMsg)
|
|
1831
|
-
return assistantMsg.content ?? ''
|
|
1832
|
-
}
|
|
1833
|
-
```
|
|
1834
|
-
|
|
1835
|
-
### A2A 代理协作
|
|
1836
|
-
|
|
1837
|
-
```ts
|
|
1838
|
-
import type { AgentExecutor } from '@h-ai/ai'
|
|
1839
|
-
import { ai } from '@h-ai/ai'
|
|
1840
|
-
|
|
1841
|
-
await ai.init({
|
|
1842
|
-
llm: { model: 'gpt-4o-mini', apiKey: process.env.API_KEY },
|
|
1843
|
-
a2a: {
|
|
1844
|
-
agentCard: {
|
|
1845
|
-
name: 'coordinator',
|
|
1846
|
-
description: '协调代理,分发任务给专业代理',
|
|
1847
|
-
url: 'https://coordinator.example.com',
|
|
1848
|
-
skills: [{ id: 'coordinate', name: '任务协调', description: '分析任务并委派给合适的代理' }],
|
|
1849
|
-
},
|
|
1850
|
-
},
|
|
1851
|
-
})
|
|
1852
|
-
|
|
1853
|
-
// 注册本地执行器
|
|
1854
|
-
const executor: AgentExecutor = {
|
|
1855
|
-
async* execute(context) {
|
|
1856
|
-
// 委派子任务给远端专业代理
|
|
1857
|
-
const analysis = await ai.a2a.callRemoteAgent(
|
|
1858
|
-
'https://analyst.example.com',
|
|
1859
|
-
context.userMessage,
|
|
1860
|
-
{ timeout: 60000 },
|
|
1861
|
-
)
|
|
1862
|
-
if (analysis.success) {
|
|
1863
|
-
yield {
|
|
1864
|
-
state: 'completed',
|
|
1865
|
-
message: {
|
|
1866
|
-
role: 'agent',
|
|
1867
|
-
parts: [{ type: 'text', text: `分析结果:${analysis.data.responseText}` }],
|
|
1868
|
-
},
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
},
|
|
1872
|
-
}
|
|
1873
|
-
ai.a2a.registerExecutor(executor)
|
|
1874
|
-
```
|
|
1875
|
-
|
|
1876
|
-
---
|
|
1877
|
-
|
|
1878
|
-
## 测试
|
|
1879
|
-
|
|
1880
|
-
```bash
|
|
1881
|
-
pnpm --filter @h-ai/ai test
|
|
1882
|
-
```
|
|
666
|
+
源码 JSDoc 的公开示例从实际包入口导入,内部工厂示例标记“模块内部上下文”,分别按真实声明编译。根目录执行 `pnpm check:jsdoc --package ai` 核验覆盖与示例类型;普通业务方法检查 HaiResult,流式与浏览器 AudioClient 保留显式抛错契约。
|
|
1883
667
|
|
|
1884
|
-
##
|
|
668
|
+
## 许可证
|
|
1885
669
|
|
|
1886
670
|
Apache-2.0
|