Skip to content

Commit 171b54b

Browse files
refactor(ai): componentize runtime and configuration with schema-validated loading (#1421)
* [refactor(ai)]componentize runtime bootstrap and wiring * [refactor(ai)]enforce validate semantics and dependency cleanup * [feat(config)]add schema-driven loader validation * [test(ai)]migrate and consolidate config/runtime/component tests
1 parent ea9df56 commit 171b54b

100 files changed

Lines changed: 8480 additions & 2396 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ai/.env.example

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,80 @@
1-
HTTPS_PROXY=http://127.0.0.1:7890
2-
HTTP_PROXY=http://127.0.0.1:7890
3-
4-
GEMINI_API_KEY=YOUR_API_KEY
5-
SILICONFLOW_API_KEY=YOUR_API_KEY
6-
DASHSCOPE_API_KEY=YOUR_API_KEY
7-
PINECONE_API_KEY=YOUR_API_KEY
8-
COHERE_API_KEY=YOUR_API_KEY
1+
# ==============================================================================
2+
# AI 服务环境变量配置
3+
# ==============================================================================
4+
#
5+
# 本文件用于存储 AI 服务所需的敏感信息(如 API 密钥)
6+
#
7+
# 使用方法:
8+
# 1. 复制本文件为 .env:cp .env.example .env
9+
# 2. 修改 .env 文件中的值,填入您的实际 API 密钥
10+
# 3. 运行程序时,godotenv 会自动加载 .env 文件到环境变量
11+
# 4. 配置文件中的 ${VAR_NAME} 会被自动替换为对应的环境变量值
12+
#
13+
# 示例:
14+
# config/models/models.yaml 中使用:
15+
# dashscope:
16+
# api_key: "${DASHSCOPE_API_KEY}"
17+
#
18+
# 程序启动时会自动将 ${DASHSCOPE_API_KEY} 替换为环境变量的值
19+
#
20+
# ==============================================================================
21+
22+
# HTTPS Proxy Configuration (可选)
23+
# 如果需要通过代理访问 API 服务,请取消注释并配置
24+
# HTTPS_PROXY=http://127.0.0.1:7890
25+
# HTTP_PROXY=http://127.0.0.1:7890
26+
27+
# ==============================================================================
28+
# Schema Configuration (必填)
29+
# ==============================================================================
30+
31+
# Schema Directory (必填)
32+
# 指定 JSON Schema 文件的目录路径
33+
#
34+
# 重要: 此配置必须通过环境变量设置,不能在 YAML 配置文件中设置
35+
# 原因: 避免 YAML 解析和 Schema 验证的循环依赖问题
36+
#
37+
# 支持相对路径(相对于 config.yaml)和绝对路径
38+
#
39+
# 默认值: schema/json (相对于项目根目录)
40+
SCHEMA_DIR=schema/json
41+
42+
# ==============================================================================
43+
# AI Model API Keys
44+
# ==============================================================================
45+
# 模型提供商的 API 密钥
46+
# 至少需要配置一个 LLM 提供商的 API 密钥才能使用 AI 服务
47+
48+
# 通义千问 (Dashscope)
49+
# 获取地址: https://dashscope.console.aliyun.com/
50+
# 支持的模型: qwen-max, qwen-plus, qwen-flash, qwen3-coder-plus
51+
DASHSCOPE_API_KEY=your_dashscope_api_key_here
52+
53+
# Google Gemini
54+
# 获取地址: https://makersuite.google.com/app/apikey
55+
# 支持的模型: gemini-pro, gemini-pro-vision
56+
GEMINI_API_KEY=your_gemini_api_key_here
57+
58+
# SiliconFlow
59+
# 获取地址: https://cloud.siliconflow.cn/account/ak
60+
# 支持的模型: gpt-3.5-turbo, gpt-4
61+
SILICONFLOW_API_KEY=your_siliconflow_api_key_here
62+
63+
# ==============================================================================
64+
# Vector Database API Keys (可选)
65+
# ==============================================================================
66+
67+
# Pinecone Vector Database (可选)
68+
# 获取地址: https://www.pinecone.io/
69+
# 用于 RAG 系统的向量存储
70+
PINECONE_API_KEY=your_pinecone_api_key_here
71+
72+
# ==============================================================================
73+
# Rerank Service API Keys (可选)
74+
# ==============================================================================
75+
76+
# Cohere Rerank (可选)
77+
# 获取地址: https://dashboard.cohere.com/
78+
# 用于 RAG 系统的结果重排序
79+
COHERE_API_KEY=your_cohere_api_key_here
80+

ai/TEST_CHECKLIST_V7.md

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# AI 模块单元测试 TODO LIST
2+
3+
> 版本:v7(三层校验重构统一版:Parser / Loader+Schema / Runtime / Component)
4+
5+
## 快速开始
6+
7+
```bash
8+
go test -race -v ./config/... ./runtime/... ./component/... ./test/... ./cmd/...
9+
```
10+
11+
---
12+
13+
## Tier 1:Parser(语法层,4个)
14+
15+
### Config Parser
16+
17+
**文件**: `config/loader_test.go`
18+
19+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
20+
|---|---------|------|------|------|---------|
21+
| 1 | `TestLoader_MainConfig_ParseError` || 主配置 YAML 语法错误 | 非法 `config.yaml`(如缺失冒号) | 返回 `parse error`,不进入结构校验 |
22+
| 2 | `TestLoader_Component_ParseError` || 组件配置 YAML 语法错误 | 非法组件 YAML | 返回 `parse error`,不进入结构校验 |
23+
| 3 | `TestLoader_MainConfig_ParseError_Priority` || 语法错误优先级 | 同时包含结构问题与语法错误的主配置 | 优先返回 `parse error` |
24+
| 4 | `TestLoader_Component_ParseError_Priority` || 语法错误优先级 | 同时包含结构问题与语法错误的组件配置 | 优先返回 `parse error` |
25+
26+
---
27+
28+
## Tier 2:Loader + Schema(结构层,12个)
29+
30+
### Main Config Structural
31+
32+
**文件**: `config/loader_test.go`
33+
34+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
35+
|---|---------|------|------|------|---------|
36+
| 5 | `TestLoader_MainConfig_UnknownField` || 主配置 unknown field 拒绝 | `config.yaml` 含未定义字段 | 返回 `structural error` |
37+
| 6 | `TestLoader_MainConfig_ComponentsTypeInvalid` || components 项类型约束 | `components.x` 为对象/数字 | 返回 `structural error`(仅允许 string/[]string) |
38+
| 7 | `TestLoader_MainConfig_ComponentsArrayItemInvalid` || components 数组项类型约束 | `components.x``["a.yaml", 1]` | 返回 `structural error` |
39+
| 8 | `TestLoader_MainConfig_DefaultSchemaDir` || SCHEMA_DIR 默认路径 | 环境变量未设置 | 默认使用 `schema/json` 成功加载 |
40+
41+
### Component Structural
42+
43+
**文件**: `config/loader_test.go`
44+
45+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
46+
|---|---------|------|------|------|---------|
47+
| 9 | `TestLoader_Component_MissingType` || 组件 type 必填 | 组件 YAML 缺失 `type` | 返回 `structural error` |
48+
| 10 | `TestLoader_Component_MissingSpec` || 组件 spec 必填 | 组件 YAML 缺失 `spec` | 返回 `structural error` |
49+
| 11 | `TestLoader_Component_UnknownTopField` || top-level unknown field 拒绝 | 组件 YAML 含未定义字段 | 返回 `structural error` |
50+
| 12 | `TestLoader_Component_DefaultInjection_Server` || server 默认值注入 | 仅给最小 server 配置 | decode 后包含 port/host/timeout 默认值 |
51+
| 13 | `TestLoader_Component_DefaultInjection_Agent` || agent 阶段默认值注入 | stage 省略 temperature/top_p 等 | decode 后包含默认值 |
52+
53+
### Conditional Required
54+
55+
**文件**: `config/loader_test.go`
56+
57+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
58+
|---|---------|------|------|------|---------|
59+
| 14 | `TestLoader_Tools_MCPEnabled_RequireHost` || 条件必填(tools) | `enable_mcp_tools=true` 且缺 `mcp_host_name` | 返回 `structural error` |
60+
| 15 | `TestLoader_RAG_RerankerEnabled_RequireAPIKey` || 条件必填(rag) | `reranker.enabled=true` 且缺 `api_key` | 返回 `structural error` |
61+
| 16 | `TestLoader_RAG_Splitter_OneOfBranchValidation` || oneOf 分支结构约束 | splitter spec 与 type 不匹配 | 返回 `structural error` |
62+
63+
---
64+
65+
## Tier 3:Runtime 调度层(6个)
66+
67+
### Runtime Orchestration
68+
69+
**文件**: `runtime/runtime_test.go`
70+
71+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
72+
|---|---------|------|------|------|---------|
73+
| 17 | `TestRuntime_RegisterFactory_Duplicate` || 重复注册覆盖 | 同类型注册两次 | 第二次覆盖生效;包含重复注册提示 |
74+
| 18 | `TestRuntime_RegisterFactory_Concurrent` || 并发注册安全 | 100 goroutine 并发注册 | 无 data race;数量正确 |
75+
| 19 | `TestRuntime_GetFactoryFn_NotFound` || 未注册工厂 | 类型名 `test` | 返回 error,包含 `not registered` |
76+
| 20 | `TestRuntime_GetComponent_NotFound` || 未注册组件 | 名称 `agent` | 返回 error,包含 `component not found` |
77+
| 21 | `TestRuntime_ComponentInitOrder` | Stub Component | 初始化顺序 | 注册多个工厂 |`factoryOrder` 顺序 `Validate -> Init` |
78+
| 22 | `TestBootstrap_ValidateFailStopsInit` | Stub Component | 语义失败中断 | 某组件 `Validate()` 返回 error | `Bootstrap` 返回 `failed to validate <name>`,不执行 Init |
79+
80+
---
81+
82+
## Tier 4:Component 语义层(8个)
83+
84+
### Component Validate
85+
86+
**文件**: `component/*/test/*_test.go`
87+
88+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
89+
|---|---------|------|------|------|---------|
90+
| 23 | `TestServerComponent_Validate_PortRange` || server 端口语义 | `port=70000` | `Validate()` 返回 error |
91+
| 24 | `TestServerComponent_Validate_TimeoutPositive` || server 超时语义 | `read_timeout<=0``write_timeout<=0` | `Validate()` 返回 error |
92+
| 25 | `TestMemoryComponent_Validate_MaxTurns` || memory 轮次语义 | `max_turns<=0` | `Validate()` 返回 error |
93+
| 26 | `TestToolsComponent_Validate_MCPConfig` || tools MCP 语义 | MCP enabled 且 host 为空 | `Validate()` 返回 error |
94+
| 27 | `TestModelsComponent_Validate_Providers` || models 语义一致性 | providers 为空或 base_url 为空 | `Validate()` 返回 error |
95+
| 28 | `TestRAGComponent_Validate_SplitterSemantic` || rag 分块语义 | `overlap_size >= chunk_size` | `Validate()` 返回 error |
96+
| 29 | `TestAgentComponent_Validate_StageFlowType` || agent 阶段语义 | 非法 `flow_type` | `Validate()` 返回 error |
97+
| 30 | `TestAgentComponent_Validate_StagePromptRequired` || agent 阶段语义 |`prompt_file` | `Validate()` 返回 error |
98+
99+
---
100+
101+
## Tier 5:Business Workflows(业务流程保留项,8个)
102+
103+
### RAG Workflow
104+
105+
**文件**: `component/rag/test/workflow_test.go`
106+
107+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
108+
|---|---------|------|------|------|---------|
109+
| 31 | `TestRAGWorkflow_Index_Retrieve` | Mock Retriever/Indexer | 索引后可检索 | 文档 `Dubbo is RPC`,查询 `RPC` | 检索结果包含 `Dubbo` |
110+
| 32 | `TestRAGWorkflow_Split_Index` | Mock Splitter/Indexer | 分块后索引 | 长文档 | `len(chunks)>1` 且全部进入索引 |
111+
| 33 | `TestRAGWorkflow_Namespace` | Mock Retriever | 命名空间隔离 | ns1/ns2 各索引文档 | ns1 查询不返回 ns2 内容 |
112+
| 34 | `TestRAG_Retrieve_EmptyQuery` | Mock Retriever | 空查询处理 | `queries=nil` | 返回空 map(非nil),无 error |
113+
114+
### Agent Workflow
115+
116+
**文件**: `component/agent/react/test/flow_test.go`
117+
118+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
119+
|---|---------|------|------|------|---------|
120+
| 35 | `TestActFlow_GeneralInquiry_NoTools` | Mock Prompt | 一般询问不调工具 | `Intent=GeneralInquiry` | 返回 `ToolOutputs``len(Outputs)=0` |
121+
| 36 | `TestActFlow_WithToolCall_ReturnsToolOutputs` | Mock Prompt+Tool | 工具调用主流程 | `Intent=PerformanceInvestigation` | 返回 `ToolOutputs`,至少1条结果 |
122+
| 37 | `TestActFlow_ToolErrorHandling` | Mock Prompt/工具 | 工具错误处理 | 工具返回 error | 返回 error,包含工具名 |
123+
| 38 | `TestThinkFlow_ExecuteError_NoNilDeref` | Mock Prompt | think 异常路径健壮性 | `Execute` 返回 error | 不应因 `resp.Text()` 引发 nil deref |
124+
125+
---
126+
127+
## Tier 6:并发与边界(保留项,8个)
128+
129+
### Memory Concurrency & State
130+
131+
**文件**: `component/memory/test/history_test.go`
132+
133+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
134+
|---|---------|------|------|------|---------|
135+
| 39 | `TestHistoryMemory_AddHistory_UserMessage` || 添加用户消息 | session-1 + user message | 进入当前 turn 的 `UserMessages` |
136+
| 40 | `TestHistoryMemory_NextTurn_ArchivesCurrentTurn` || 推进会话归档当前 turn | 已有1个turn | 旧 turn 进入 history,window 前移 |
137+
| 41 | `TestHistoryMemory_NextTurn_WhenSessionFull` || 会话窗口满时行为 | 将窗口填满后 `NextTurn` | 返回 error,包含 `context is full` |
138+
| 42 | `TestHistoryMemory_ConcurrentAddHistory` || 并发写历史安全 | 100 goroutine 写入 | 无 panic,无 race |
139+
| 43 | `TestHistoryMemory_ConcurrentReadWrite` || 并发读写安全 | 10写+10读 goroutine | 不 panic,无 data race |
140+
| 44 | `TestHistoryMemory_NextTurn_EmptyWindowSafety` || 空窗口推进安全 | session 被 pop 空后重复 `NextTurn` | 固定当前 panic 风险或修复后断言 error |
141+
142+
### Runtime/Bootstrap Boundary
143+
144+
**文件**: `runtime/runtime_test.go`, `component/*/test/*_test.go`
145+
146+
| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 |
147+
|---|---------|------|------|------|---------|
148+
| 45 | `TestBootstrap_MissingFactoryForConfiguredType` || 配置类型无工厂 | 配置 type 未注册 | error 包含 `no factory for` |
149+
| 46 | `TestRuntime_GetRuntime_NotInitialized` || 全局Runtime未初始化 | 直接调用 `GetRuntime()` | 触发 panic `Runtime not initialized` |
150+
151+
---
152+
153+
## 按层统计
154+
155+
| 层/域 | 数量 | 说明 |
156+
|------|------|------|
157+
| Parser | 4 | 只验证语法失败归属 |
158+
| Loader+Schema | 12 | 只验证结构校验、条件必填、默认注入 |
159+
| Runtime | 6 | 只验证调度顺序与生命周期 |
160+
| Component Semantic | 8 | 只验证语义规则 |
161+
| Business Workflows | 8 | 保留原有高价值业务流程测试 |
162+
| Robustness & Concurrency | 8 | 保留原有并发与边界保护测试 |
163+
| **总计** | **46** | 三层重构 + 原有单测统一清单 |
164+
165+
---
166+
167+
## 编写规范
168+
169+
- 错误断言按层级关键字匹配:
170+
- 语法层:`parse error`
171+
- 结构层:`structural error`
172+
- 语义层:`failed to validate` 或组件语义错误信息
173+
- 业务流程测试不承担结构层职责断言。
174+
- 并发测试建议配合 `-race` 在 CI 中执行。
175+
176+
---
177+
178+
## Mock 说明
179+
180+
| Mock对象 | 用途 | 实现方式 |
181+
|---------|------|---------|
182+
| Mock Genkit Registry | 模拟模型注册与组件初始化 | 使用 `testutils.CreateMockGenkitRegistry()` 或等价 stub |
183+
| Mock Prompt | 模拟 LLM 返回(含 ToolRequests) | 自定义 `MockPrompt` / stub prompt,覆盖成功与失败分支 |
184+
| Mock Tool | 模拟工具调用成功/失败/超时 | 本地 mock struct + 可配置返回值 |
185+
| Mock Retriever/Indexer/Splitter | 控制 RAG 行为并断言参数 | 本地 mock struct + 调用计数/入参记录 |
186+
| Stub Component | 验证 Runtime 初始化顺序/错误传播 | 自定义实现 `runtime.Component`,可注入 `Validate/Init/Start/Stop` 行为 |
187+
| 临时配置文件夹(Fixture Dir) | 隔离配置输入与路径解析 | `t.TempDir()` 下写入最小 YAML/Schema 夹具 |
188+
189+
---
190+
191+
## Go 表格驱动单测编写规范
192+
193+
### 1) 基本模板
194+
195+
```go
196+
func TestXXX(t *testing.T) {
197+
tests := []struct {
198+
name string
199+
input any
200+
wantErr bool
201+
errLike string
202+
}{
203+
{
204+
name: "valid case",
205+
input: ...,
206+
wantErr: false,
207+
},
208+
{
209+
name: "invalid case",
210+
input: ...,
211+
wantErr: true,
212+
errLike: "structural error",
213+
},
214+
}
215+
216+
for _, tt := range tests {
217+
t.Run(tt.name, func(t *testing.T) {
218+
// arrange
219+
// act
220+
// assert
221+
})
222+
}
223+
}
224+
```
225+
226+
### 2) 断言规范
227+
228+
- 错误断言使用关键子串匹配,避免脆弱的全量字符串匹配。
229+
- 每条测试只验证一个主行为(单一断言目标)。
230+
- 分层测试中禁止跨层断言:
231+
- Parser 测试不验证语义
232+
- Loader 测试不验证组件语义
233+
- Component 测试假设结构已通过
234+
235+
### 3) 并发与稳定性
236+
237+
- 并发测试统一使用 `go test -race` 验证。
238+
- 避免真实网络/端口依赖,全部使用本地 mock。
239+
- 使用 `t.Helper()` 封装重复构建逻辑,提高可读性。
240+
241+
### 4) 夹具与命名
242+
243+
- 测试名采用 `Test<模块>_<行为>_<预期>` 风格。
244+
- 配置夹具优先最小化:只保留触发当前断言所需字段。
245+
- 使用 `t.TempDir()` 管理临时文件,避免污染仓库。
246+
247+
### 5) 回归要求
248+
249+
- 每个缺陷修复至少补 1 条失败重现用例。
250+
- 先写失败断言,再修复实现(红-绿流程)。

0 commit comments

Comments
 (0)