Compare commits
5
Commits
v0.1.0
...
9e476e79bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e476e79bb | ||
|
|
3bd135ec98 | ||
|
|
76f3235ed7 | ||
|
|
6315f2d008 | ||
|
|
fba78f5f33 |
@@ -0,0 +1,522 @@
|
||||
# Phase 5:热身准备 — 实施方案
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
Phase 5 是 v0.2.0 发布周期的**热身准备阶段**,包含三个互不依赖的 Step,为后续 Phase 6-12 的端到端集成提供基础设施。
|
||||
|
||||
**核心目标**:
|
||||
- 为 Phase 8(端到端示例)提供零 API key 的运行路径(Ollama)
|
||||
- 为公共枚举的向后兼容性加上编译期护栏(`#[non_exhaustive]`)
|
||||
- 为 Provider 构造提供统一的超时与重试配置入口(`ProviderConfig` 扩展)
|
||||
|
||||
三个 Step 之间**无依赖关系**,但出于实现效率考虑,按 **5.2 → 5.3 → 5.1** 顺序执行。理由:5.2 先新增 `Ollama` 枚举变体,5.3 再加 `#[non_exhaustive]`,避免枚举标记后添加变体需要在外部 crate 加 `_ =>` 兜底分支的困扰。
|
||||
|
||||
## 2. 需求分析
|
||||
|
||||
### Step 5.2 — Ollama Provider
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **需求** | 新增 `OllamaProvider`,newtype 包装 `GenericOpenaiProvider`,默认连接本地 Ollama 实例 |
|
||||
| **优先级** | P0 — 为 Phase 8 端到端示例提供无需 API key 的运行路径 |
|
||||
| **预期交付物** | `src/llm/provider/ollama.rs` 新建文件;`ProviderType` 新增 `Ollama` 变体 |
|
||||
| **代码量** | ~55 行 |
|
||||
|
||||
### Step 5.3 — `#[non_exhaustive]` 前置标记
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **需求** | 为 4 个公共枚举添加 `#[non_exhaustive]` 属性,避免后续新增变体时破坏下游 match |
|
||||
| **优先级** | P1 — 编译期兼容性保障 |
|
||||
| **预期交付物** | 修改 4 个枚举定义,各加一行属性 |
|
||||
| **代码量** | ~4 行 |
|
||||
|
||||
### Step 5.1 — ProviderConfig 扩展
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **需求** | `ProviderConfig` 新增 `timeout_secs` 和 `max_retries` 字段;实现 `Default`、`from_env()` 构造;timeout 传导到各 Provider HTTP Client |
|
||||
| **优先级** | P0 — 与 Roadmap 一致,Phase 8(MVP 出口)依赖 from_env |
|
||||
| **预期交付物** | `ProviderConfig` 扩展;`create_provider()` 超时注入;`from_env()` + 单元测试 |
|
||||
| **代码量** | ~60 行 + 测试 |
|
||||
|
||||
## 3. 方案设计
|
||||
|
||||
### 3.1 Step 5.2 — Ollama Provider(先执行)
|
||||
|
||||
#### 改动文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `src/llm/provider/ollama.rs` | **新建** | OllamaProvider newtype 包装 |
|
||||
| `src/llm/provider.rs` | 修改 | `ProviderType` 新增 `Ollama` 变体;`FromStr` 加解析;`create_provider()` 加分支 |
|
||||
| `src/llm/provider/mod.rs` 或其他模块注册文件 | 修改(如需要) | 注册 `pub mod ollama` |
|
||||
|
||||
#### 关键代码
|
||||
|
||||
**`src/llm/provider/ollama.rs`**(新建):
|
||||
|
||||
```rust
|
||||
//! Ollama Provider —— OpenAI-compatible 协议的 newtype 包装,零 API key。
|
||||
//!
|
||||
//! 默认 base_url = `http://localhost:11434/v1`,空 api_key 也可工作。
|
||||
//! 实现方式同 DeepSeekProvider / QwenProvider,共享 GenericOpenaiProvider 的 HTTP/SSE/转换逻辑。
|
||||
|
||||
use reqwest::Client;
|
||||
use std::pin::Pin;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_core::Stream;
|
||||
|
||||
use super::openai::GenericOpenaiProvider;
|
||||
use super::{LlmProvider, ProviderCapabilities};
|
||||
use crate::llm::error::LlmError;
|
||||
use crate::llm::types::request_v2::MessageRequest;
|
||||
use crate::llm::types::response_v2::{MessageResponse, StreamEvent};
|
||||
|
||||
pub struct OllamaProvider(pub GenericOpenaiProvider);
|
||||
|
||||
impl OllamaProvider {
|
||||
pub fn new(base_url: String, api_key: String, model: String) -> Self {
|
||||
let url = if base_url.is_empty() {
|
||||
"http://localhost:11434/v1".to_string()
|
||||
} else {
|
||||
base_url
|
||||
};
|
||||
Self(GenericOpenaiProvider::new_with_name(
|
||||
url,
|
||||
api_key,
|
||||
model,
|
||||
"ollama",
|
||||
))
|
||||
}
|
||||
|
||||
/// 替换默认 HTTP Client(用于 timeout 注入等场景)。
|
||||
/// 与 `OpenaiChatProvider::with_client` 和 `DeepSeekProvider::with_client` 一致。
|
||||
pub fn with_client(self, client: Client) -> Self {
|
||||
Self(self.0.with_client(client))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmProvider for OllamaProvider {
|
||||
async fn chat(&self, request: MessageRequest) -> Result<MessageResponse, LlmError> {
|
||||
self.0.chat(request).await
|
||||
}
|
||||
|
||||
async fn chat_stream(
|
||||
&self,
|
||||
request: MessageRequest,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError> {
|
||||
self.0.chat_stream(request).await
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ProviderCapabilities {
|
||||
let mut caps = self.0.capabilities();
|
||||
caps.provider_name = "ollama";
|
||||
caps
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`src/llm/provider.rs`** 的修改:
|
||||
|
||||
```rust
|
||||
// ProviderType 新增变体
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProviderType {
|
||||
OpenaiChat,
|
||||
OpenaiResponse,
|
||||
Anthropic,
|
||||
DeepSeek,
|
||||
Qwen,
|
||||
/// Ollama(本地),默认 base_url = `http://localhost:11434/v1`。
|
||||
Ollama,
|
||||
}
|
||||
|
||||
// FromStr 加解析
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
// ... 已有条目 ...
|
||||
"ollama" => Ok(ProviderType::Ollama),
|
||||
_ => Err(format!("未知的 Provider 类型: {s}")),
|
||||
}
|
||||
}
|
||||
|
||||
// create_provider() 加分支
|
||||
// Step 5.2 阶段仅展示基本构造。Step 5.1(ProviderConfig 扩展)
|
||||
// 执行到此分支时,将同步补充 with_client 链式调用注入 timeout:
|
||||
//
|
||||
// let client = Client::builder()
|
||||
// .timeout(Duration::from_secs(config.timeout_secs))
|
||||
// .build()?;
|
||||
// Ok(Box::new(
|
||||
// ollama::OllamaProvider::new(config.base_url, config.api_key, config.model)
|
||||
// .with_client(client),
|
||||
// ))
|
||||
ProviderType::Ollama => Ok(Box::new(ollama::OllamaProvider::new(
|
||||
config.base_url,
|
||||
config.api_key,
|
||||
config.model,
|
||||
))),
|
||||
```
|
||||
|
||||
#### 集成方式
|
||||
|
||||
OllamaProvider 的 newtype 包装模式与 `DeepSeekProvider`、`QwenProvider` 完全一致,`LlmProvider` trait 委托给 `self.0`。`capabilities().provider_name` 返回 `"ollama"`。
|
||||
|
||||
### 3.2 Step 5.3 — `#[non_exhaustive]` 前置标记
|
||||
|
||||
#### 改动文件清单
|
||||
|
||||
| 文件 | 行号 | 枚举 | 操作 |
|
||||
|------|------|------|------|
|
||||
| `src/llm/provider.rs` | ~21 | `ProviderType` | 加 `#[non_exhaustive]` |
|
||||
| `src/llm/types/response_v2.rs` | ~22 | `StopReason` | 加 `#[non_exhaustive]` |
|
||||
| `src/llm/types/shared.rs` | ~16 | `FinishReason` | 加 `#[non_exhaustive]` |
|
||||
| `src/memory/store.rs` | ~35 | `EvictionPolicy` | 加 `#[non_exhaustive]` |
|
||||
|
||||
**排除清单**:`SlotMode`。
|
||||
|
||||
**决策理由**:`SlotMode` 枚举在 Phase 10(`src/llm/context.rs`)中才实际定义,Phase 5 尚不存在此类型。`#[non_exhaustive]` 无法标注不存在的枚举,因此排除标注。Roadmap(v0.2.0 §Phase 5 Step 5.3)列出的 `SlotMode`(预置) 推迟到 Phase 10 实现时一并添加。
|
||||
|
||||
#### 关键代码
|
||||
|
||||
每个枚举在 `derive` 上方或下方加一行属性:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum ProviderType {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 影响分析
|
||||
|
||||
- `#[non_exhaustive]` 是纯编译期属性,不影响运行时行为
|
||||
- 同一 crate 内的 exhaustive match 不受影响(同 crate 可穷举)
|
||||
- 下游 crate 的 match 必须加 `_ =>` 兜底分支,这是期望行为——确保未来新增变体时不会 silent break
|
||||
- **单向门**:此步骤一旦通过 `v0.2.0` 发布到公共 API 后,**不可回退**。回退意味着移除 `#[non_exhaustive]`,可能破坏已添加 `_ =>` 的下游代码。因此必须在发布前完成并确认所有枚举变体正确
|
||||
|
||||
### 3.3 Step 5.1 — ProviderConfig 扩展(最后执行)
|
||||
|
||||
#### 改动文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `src/llm/provider.rs` | 修改 | `ProviderConfig` 加字段;加 `impl Default`;加 `from_env()`;`create_provider` 注入 timeout |
|
||||
| `src/llm/provider/openai.rs` | 修改 | `GenericOpenaiProvider` 新增 `timeout_secs` 字段;`new_with_name` 接受 timeout 参数;`map_reqwest_error` 参数化 |
|
||||
| `src/llm/provider/anthropic.rs` | 修改 | 新增 `timeout_secs` 字段;`new()` 接受 timeout 参数;`map_reqwest_error` 参数化 |
|
||||
| `src/llm/provider/anthropic.rs` | 修改 | 新增 `with_timeout()` 方法(返回 `Result<Self, LlmError>`) |
|
||||
| `src/llm/provider/openai_compat.rs` | 修改 | `DeepSeekProvider` 和 `QwenProvider` 新增公开 `with_client()` 方法 |
|
||||
| `src/llm/provider/ollama.rs` | 修改 | `OllamaProvider` 新增公开 `with_client()` 方法 |
|
||||
| `Cargo.toml` | 修改 | 加 `temp_env` dev-dependency |
|
||||
| 测试文件(`provider.rs` 内联或独立) | 新增 | `from_env` 单元测试 + timeout 传导集成测试 |
|
||||
|
||||
#### 数据结构
|
||||
|
||||
```rust
|
||||
/// Provider 构造参数 —— 通用 base_url + api_key + model + timeout/retry 配置。
|
||||
pub struct ProviderConfig {
|
||||
pub base_url: String,
|
||||
pub api_key: String,
|
||||
pub model: String,
|
||||
/// 请求超时秒数(默认 30)。应用于 Provider 的 HTTP Client 级别。
|
||||
pub timeout_secs: u64,
|
||||
/// 最大重试次数(默认 3)。当前此字段仅由 `from_env()` 采集,
|
||||
/// 实际重试逻辑由 `CycleConfig.retry.max_retries` 控制。
|
||||
/// 未来可合并到统一的 retry 配置。
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
impl Default for ProviderConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: String::new(),
|
||||
api_key: String::new(),
|
||||
model: String::new(),
|
||||
timeout_secs: 30,
|
||||
max_retries: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProviderConfig {
|
||||
/// 从环境变量构造 ProviderConfig。
|
||||
///
|
||||
/// 必填变量:
|
||||
/// - `{prefix}_BASE_URL`
|
||||
/// - `{prefix}_API_KEY`
|
||||
/// - `{prefix}_MODEL`
|
||||
///
|
||||
/// 可选变量(有默认值):
|
||||
/// - `{prefix}_TIMEOUT_SECS`(默认 30)
|
||||
/// - `{prefix}_MAX_RETRIES`(默认 3)
|
||||
pub fn from_env(prefix: &str) -> Result<Self, String> {
|
||||
let base_url = std::env::var(format!("{prefix}_BASE_URL"))
|
||||
.map_err(|_| format!("{prefix}_BASE_URL 环境变量未设置"))?;
|
||||
let api_key = std::env::var(format!("{prefix}_API_KEY"))
|
||||
.map_err(|_| format!("{prefix}_API_KEY 环境变量未设置"))?;
|
||||
let model = std::env::var(format!("{prefix}_MODEL"))
|
||||
.map_err(|_| format!("{prefix}_MODEL 环境变量未设置"))?;
|
||||
let timeout_secs = match std::env::var(format!("{prefix}_TIMEOUT_SECS")) {
|
||||
Ok(v) => v.parse().unwrap_or_else(|_| {
|
||||
tracing::warn!("{prefix}_TIMEOUT_SECS='{v}' 解析失败,使用默认值 30");
|
||||
30
|
||||
}),
|
||||
Err(_) => 30,
|
||||
};
|
||||
let max_retries = match std::env::var(format!("{prefix}_MAX_RETRIES")) {
|
||||
Ok(v) => v.parse().unwrap_or_else(|_| {
|
||||
tracing::warn!("{prefix}_MAX_RETRIES='{v}' 解析失败,使用默认值 3");
|
||||
3
|
||||
}),
|
||||
Err(_) => 3,
|
||||
};
|
||||
|
||||
// ponytail: max_retries 当前仅采集,不传入 Provider。
|
||||
// 实际重试由 CycleConfig.retry.max_retries 控制。
|
||||
// 此 warn 在应用启动时通常只触发一次,多次调用 from_env 时
|
||||
// 重复输出的风险低。如有噪声,可改用 std::sync::Once 控制。
|
||||
if max_retries != 3 {
|
||||
tracing::warn!(
|
||||
"ProviderConfig.max_retries={} 已采集但当前未生效;\
|
||||
重试次数由 CycleConfig.retry.max_retries 控制",
|
||||
max_retries,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
base_url,
|
||||
api_key,
|
||||
model,
|
||||
timeout_secs,
|
||||
max_retries,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Timeout 传导模式
|
||||
|
||||
在 `create_provider()` 中,对基于 `GenericOpenaiProvider` 的 Provider(OpenAI / DeepSeek / Qwen / Ollama),通过同一模式注入 timeout:构造带 timeout 的 `Client` 后调用 `with_client(client)`。
|
||||
|
||||
所有 OpenAI-compatible 分支新增的 `with_client()` 公开方法:
|
||||
|
||||
| Provider | 方法 | 位置 |
|
||||
|----------|------|------|
|
||||
| `OpenaiChatProvider` | 已有 `with_client(Client) -> Self` | `openai.rs` |
|
||||
| `DeepSeekProvider` | 新增 `with_client(Client) -> Self` | `openai_compat.rs` |
|
||||
| `QwenProvider` | 新增 `with_client(Client) -> Self` | `openai_compat.rs` |
|
||||
| `OllamaProvider` | 新增 `with_client(Client) -> Self` | `ollama.rs`(新建文件) |
|
||||
|
||||
**关于 `new_with_client` 的说明**:`DeepSeekProvider` 和 `QwenProvider` 当前已有测试用的 `new_with_client(base_url, api_key, model, client)` 方法(通过 `inner.http_client = client` 直接写字段)。新增 `with_client` 后,`new_with_client` 应重构为 `Self::new(base_url, api_key, model).with_client(client)` 代理,统一走公开 API 路径。
|
||||
|
||||
代码示例(以 DeepSeek 为例,OpenAI/Qwen/Ollama 模式完全一致):
|
||||
|
||||
```rust
|
||||
ProviderType::DeepSeek => {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(config.timeout_secs))
|
||||
.build()
|
||||
.map_err(|e| LlmError::Other(format!("创建 HTTP 客户端失败: {e}")))?;
|
||||
Ok(Box::new(
|
||||
openai_compat::DeepSeekProvider::new(
|
||||
config.base_url,
|
||||
config.api_key,
|
||||
config.model,
|
||||
)
|
||||
.with_client(client),
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
Anthropic 由于需要保留 `default_headers`,使用独立的 `with_timeout` 模式:
|
||||
|
||||
AnthropicProvider 新增 `with_timeout` 方法:
|
||||
|
||||
```rust
|
||||
impl AnthropicProvider {
|
||||
/// 替换默认 HTTP Client 的超时配置。
|
||||
///
|
||||
/// ⚠️ 副作用:此方法**完全重建** `http_client`,调用后原有通过 `with_client`
|
||||
/// 注入的 Client 将被替换。headers 逻辑与 `new()` 中的构造保持一致。
|
||||
pub fn with_timeout(mut self, secs: u64) -> Result<Self, LlmError> {
|
||||
// ponytail: 重建 http_client 时保留已有默认 headers(x-api-key / anthropic-version)。
|
||||
// 如后续 AnthropicProvider 的 headers 变为动态,此方法需同步更新。
|
||||
let key_header = HeaderValue::from_str(&self.api_key)
|
||||
.map_err(|_| LlmError::Other("Anthropic API key 包含无效的 HTTP 头部字符".into()))?;
|
||||
let version_header = HeaderValue::from_static("2023-06-01");
|
||||
|
||||
self.http_client = Client::builder()
|
||||
.timeout(Duration::from_secs(secs))
|
||||
.default_headers({
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-api-key", key_header);
|
||||
headers.insert("anthropic-version", version_header);
|
||||
headers
|
||||
})
|
||||
.build()
|
||||
.map_err(|e| LlmError::Other(format!("创建 Anthropic HTTP 客户端失败: {e}")))?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `map_reqwest_error` 中的硬编码超时修复
|
||||
|
||||
`openai.rs` 和 `anthropic.rs` 中的 `map_reqwest_error` 辅助函数当前在超时错误中返回硬编码的 `Duration::from_secs(120)`:
|
||||
|
||||
```rust
|
||||
// 现状 —— 硬编码 120s,与可配置 timeout 脱节
|
||||
LlmError::Timeout { duration: Duration::from_secs(120) }
|
||||
```
|
||||
|
||||
**修复方式**:采用**方案 A**——在 Provider struct 中存储 `timeout_secs` 字段,`map_reqwest_error` 读取该字段的值而非硬编码 120s。
|
||||
|
||||
```rust
|
||||
// 修复后 —— 参数化,从 Provider 存储的 timeout_secs 读取
|
||||
// GenericOpenaiProvider 新增 timeout_secs 字段:
|
||||
pub struct GenericOpenaiProvider {
|
||||
http_client: Client,
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
model: String,
|
||||
provider_name: &'static str,
|
||||
extra_headers: Vec<(String, String)>,
|
||||
timeout_secs: u64, // ← 新增,由 new_with_name 的参数传入
|
||||
}
|
||||
|
||||
// map_reqwest_error 使用 self.timeout_secs 而非硬编码 120:
|
||||
LlmError::Timeout { duration: Duration::from_secs(self.timeout_secs) }
|
||||
```
|
||||
|
||||
**方案 B(从 reqwest::Client 提取 timeout)已被否决**:`reqwest::Client` 不提供 timeout getter,无法从已构造的 client 中反向读取超时配置。
|
||||
|
||||
如果漏掉此修复,用户设置 `AG_LLM_TIMEOUT_SECS=60` 后超时,错误消息仍显示 "LLM 请求超时(120s)",与实际配置不符。
|
||||
|
||||
---
|
||||
|
||||
#### max_retries 说明
|
||||
|
||||
`ProviderConfig.max_retries` 当前仅由 `from_env()` 采集存储,**实际重试操作由 `CycleConfig.retry.max_retries` 控制**。两者之间的关系通过文档注释声明:
|
||||
|
||||
```rust
|
||||
/// 最大重试次数(默认 3)。当前此字段仅由 `from_env()` 采集,
|
||||
/// 实际重试逻辑由 `CycleConfig.retry.max_retries` 控制。
|
||||
/// 未来 Phase 6+ 可统一合并此字段到 CycleConfig。
|
||||
```
|
||||
|
||||
#### 测试设计
|
||||
|
||||
使用 `temp_env` 在单元测试中隔离环境变量:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn provider_config_from_env_requires_all_vars() {
|
||||
// 未设置任何变量时应返回 Err
|
||||
let result = ProviderConfig::from_env("TEST_PROVIDER");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_config_from_env_uses_defaults() {
|
||||
temp_env::with_vars([
|
||||
("TEST_PROVIDER_BASE_URL", Some("http://localhost:11434/v1")),
|
||||
("TEST_PROVIDER_API_KEY", Some("")),
|
||||
("TEST_PROVIDER_MODEL", Some("llama3")),
|
||||
], || {
|
||||
let config = ProviderConfig::from_env("TEST_PROVIDER").unwrap();
|
||||
assert_eq!(config.timeout_secs, 30);
|
||||
assert_eq!(config.max_retries, 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_config_from_env_reads_custom_timeout() {
|
||||
temp_env::with_vars([
|
||||
("TEST_PROVIDER_BASE_URL", Some("http://x")),
|
||||
("TEST_PROVIDER_API_KEY", Some("k")),
|
||||
("TEST_PROVIDER_MODEL", Some("m")),
|
||||
("TEST_PROVIDER_TIMEOUT_SECS", Some("60")),
|
||||
("TEST_PROVIDER_MAX_RETRIES", Some("5")),
|
||||
], || {
|
||||
let config = ProviderConfig::from_env("TEST_PROVIDER").unwrap();
|
||||
assert_eq!(config.timeout_secs, 60);
|
||||
assert_eq!(config.max_retries, 5);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 实现计划
|
||||
|
||||
### Step 5.2 — Ollama Provider(~55 行)
|
||||
|
||||
| 步骤 | 操作 | 验证 |
|
||||
|------|------|------|
|
||||
| 1 | 创建 `src/llm/provider/ollama.rs`,实现 `OllamaProvider` newtype | 编译通过 |
|
||||
| 2 | 在 `provider.rs` 注册 `pub mod ollama` | 编译通过 |
|
||||
| 3 | `ProviderType` 新增 `Ollama` 变体 | 编译通过 |
|
||||
| 4 | `FromStr` 加 `"ollama"` 解析 | 编译通过 |
|
||||
| 5 | `create_provider()` 加 `Ollama =>` 分支 | 编译通过 |
|
||||
| 6 | 运行 `cargo build` | 无错误 |
|
||||
|
||||
### Step 5.3 — `#[non_exhaustive]` 前置标记(~4 行)
|
||||
|
||||
| 步骤 | 操作 | 验证 |
|
||||
|------|------|------|
|
||||
| 1 | `ProviderType`(`provider.rs`)加 `#[non_exhaustive]` | 编译通过 |
|
||||
| 2 | `StopReason`(`response_v2.rs`)加 `#[non_exhaustive]` | 编译通过 |
|
||||
| 3 | `FinishReason`(`shared.rs`)加 `#[non_exhaustive]` | 编译通过 |
|
||||
| 4 | `EvictionPolicy`(`memory/store.rs`)加 `#[non_exhaustive]` | 编译通过 |
|
||||
| 5 | 运行 `cargo build --all-targets` | 无 warning |
|
||||
|
||||
### Step 5.1 — ProviderConfig 扩展(~60 行 + 测试)
|
||||
|
||||
| 步骤 | 操作 | 验证 |
|
||||
|------|------|------|
|
||||
| 1 | `ProviderConfig` 加 `timeout_secs` / `max_retries` 字段 | 编译通过 |
|
||||
| 2 | 实现 `impl Default for ProviderConfig` | 编译通过 |
|
||||
| 3 | 实现 `ProviderConfig::from_env()` | 编译通过 |
|
||||
| 4 | `GenericOpenaiProvider` 和 `AnthropicProvider` 新增 `timeout_secs` 字段,`new_with_name`/`new()` 接受 timeout 参数 | 编译通过 |
|
||||
| 5 | `map_reqwest_error` 在各 Provider 中改为从 `self.timeout_secs` 读取,移除硬编码 120s | 编译通过 |
|
||||
| 6 | `create_provider()` 中各分支注入 timeout(OpenAI-compatible 用 `Client::builder().timeout()` + `with_client`;Anthropic 用 `with_timeout()`) | 编译通过 |
|
||||
| 7 | `DeepSeekProvider`/`QwenProvider` 的 `new_with_client` 重构为 `Self::new(...).with_client(client)` 代理 | 测试通过 |
|
||||
| 8 | `Cargo.toml` 添加 `temp_env` dev-dependency | `cargo build` 通过 |
|
||||
| 8 | 添加 `from_env` 单元测试 + timeout 传导集成测试 | `cargo test` 通过 |
|
||||
| 9 | 完整验证 | 见第 6 节 |
|
||||
|
||||
## 5. 风险评估
|
||||
|
||||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||
|------|------|------|----------|
|
||||
| `create_provider()` 中 `Client::builder().build()` 返回 `Result`,当前代码使用 `.expect()`,改为 `map_err` 转为 `LlmError` 后需确保所有分支正确转换 | 编译期强制处理,遗漏分支直接报错 | 低 | `create_provider` 返回 `Result<Box<dyn LlmProvider>, LlmError>`,`map_err` 天然适配。新增的 timeout 注入路径逐一检查 |
|
||||
| `AnthropicProvider` 的 `default_headers` 在 `with_timeout` 中重建时与 `new()` 中的 headers 不一致 | Anthropic 认证失败 | 低 | `with_timeout` 方法复制 `new()` 中的 headers 构造逻辑。通过已有测试验证认证通过 |
|
||||
| Ollama 实际运行时行为差异:版本兼容性、API 路径、模型名等 | 运行时才能发现 | 中 | Phase 5 仅做类型级验证(`cargo build`),Phase 8 端到端测试时通过 Ollama mock 或真实实例验证 |
|
||||
| `max_retries` 存储了却未实际使用,造成困惑 | 开发者误以为已生效 | 中 | 通过文档注释明确声明 `max_retries` 当前仅采集,实际重试由 `CycleConfig.retry.max_retries` 控制 |
|
||||
| `temp_env` 测试在多线程并发测试中互相污染环境变量 | 偶发测试失败 | 中(Rust 默认单线程测试用 `--test-threads=1` 可避免) | 将 `from_env` 测试控制在同一测试文件,避免并行执行。必要时在 CI 中确保 `--test-threads=1` |
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
以下条件**全部满足**方可认为 Phase 5 完成:
|
||||
|
||||
- [ ] `cargo build --all-targets` 通过,无错误
|
||||
- [ ] `cargo test --all-targets` 通过,新增测试覆盖 `from_env` 的必填/选填/默认值场景
|
||||
- [ ] `cargo clippy --all-targets -- -D warnings` 通过,无任何 warning
|
||||
- [ ] `cargo doc --no-deps -D warnings` 通过,所有公共 API 有文档注释(`///`)
|
||||
- [ ] 新增文件:1(`ollama.rs`)
|
||||
- [ ] 修改文件:9(`provider.rs`、`openai.rs`、`anthropic.rs`、`openai_compat.rs`、`response_v2.rs`、`shared.rs`、`store.rs`、`Cargo.toml`、测试文件)
|
||||
- [ ] 净代码增量:~160 行
|
||||
- [ ] `ProviderType` 新增 `Ollama` 变体,`"ollama"` 字符串可解析
|
||||
- [ ] 4 个公共枚举带有 `#[non_exhaustive]` 属性
|
||||
- [ ] `ProviderConfig` 可从环境变量构造(`from_env()`),含默认值
|
||||
- [ ] timeout 值已传导到 `create_provider()` 中各 Provider 的 HTTP Client 配置
|
||||
- [ ] timeout 传导验证通过至少一个端到端 wiremock 集成测试(模拟 HTTP 服务在超时后返回 408,验证 Provider 返回 `LlmError::Timeout`)
|
||||
- [ ] `DeepSeekProvider`、`QwenProvider`、`OllamaProvider` 均有公开 `with_client()` 方法,可在 `create_provider` 中注入 timeout Client
|
||||
- [ ] `map_reqwest_error` 中不再硬编码 `Duration::from_secs(120)`,改为参数化读取
|
||||
@@ -0,0 +1,344 @@
|
||||
# 笔记:opencode 子代理调度、分发与合并及工作流推进
|
||||
|
||||
> 基于 `/Users/midnite/Samples/opencode` 源码调研,2026-07-04
|
||||
|
||||
---
|
||||
|
||||
## 一、整体架构
|
||||
|
||||
```
|
||||
LLM(主 Agent)
|
||||
│
|
||||
├── 调用 Task tool(tool call)
|
||||
│ ↓
|
||||
│ TaskTool.execute() ← packages/opencode/src/tool/task.ts
|
||||
│ │
|
||||
│ ├── agent.get() ← 查找 Agent 定义(agent.ts)
|
||||
│ ├── deriveSubagentPermission() ← 权限合并(subagent-permissions.ts)
|
||||
│ ├── sessions.create() ← 创建子 session
|
||||
│ │
|
||||
│ ├── [前台] background.wait() + background.waitForPromotion() race
|
||||
│ │ ↓ 完成
|
||||
│ │ renderOutput() → XML <task> 标签返回
|
||||
│ │
|
||||
│ └── [后台] background.start() → notify() 异步注入结果
|
||||
│
|
||||
└── 会话循环(runLoop) ← prompt.ts
|
||||
│
|
||||
├── 检测 subtask type part → handleSubtask()
|
||||
├── 检测 compaction → compaction.process()
|
||||
└── 正常流程 → LLM.stream() → processor.handleEvent()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、子代理调度(Dispatch)
|
||||
|
||||
### 2.1 三种触发入口
|
||||
|
||||
| 入口 | 触发方式 | 调用链路 |
|
||||
|------|---------|---------|
|
||||
| A — LLM 自主 | LLM 调用 `task` tool | 系统提示词中注入了 Task tool 描述 + `describeTask()` 输出子代理列表 → LLM 决策 |
|
||||
| B — `subtask` part | 消息中有 `type: "subtask"` 的 part | `handleSubtask()` 直接执行 TaskTool,不走 LLM |
|
||||
| C — `agent` part | 消息中有 `type: "agent"` 的 part | 转为"调用 task tool 带 subagent: XXX"的提示词,引导 LLM |
|
||||
|
||||
### 2.2 TaskTool.execute() 完整流程(task.ts)
|
||||
|
||||
```
|
||||
execute(params, ctx):
|
||||
1. background 开关检查(需 experimental flag)
|
||||
2. ctx.ask() 权限询问
|
||||
3. agent.get(subagent_type) 查找子代理定义
|
||||
4. task_id 存在 → sessions.get(task_id) 恢复已有子 session
|
||||
task_id 不存在 → sessions.create() 创建新子 session
|
||||
5. deriveSubagentSessionPermission() 合并权限
|
||||
6. 添加默认 deny 规则(todowrite / task)
|
||||
7. 确定 model(继承或子代理自定义)
|
||||
8. 执行 runTask() → ops.resolvePromptParts() + ops.prompt()
|
||||
9. 结果格式化为 XML ← renderOutput()
|
||||
```
|
||||
|
||||
### 2.3 关键:子 session 创建(task.ts lines 121-158)
|
||||
|
||||
```typescript
|
||||
// 权限继承
|
||||
const childPermission = deriveSubagentSessionPermission({
|
||||
parentSessionPermission: parent.permission ?? [],
|
||||
subagent: next,
|
||||
})
|
||||
|
||||
// 默认 deny 规则
|
||||
const childToolDenies = [
|
||||
// 子代理自己的 permission 没允许 todowrite → 默认 deny
|
||||
...(next.permission.some(r => r.permission === "todowrite") ? []
|
||||
: [{ permission: "todowrite", pattern: "*", action: "deny" }]),
|
||||
// 子代理自己的 permission 没允许 task → 默认 deny(防嵌套)
|
||||
...(next.permission.some(r => r.permission === "task") ? []
|
||||
: [{ permission: "task", pattern: "*", action: "deny" }]),
|
||||
// 主 agent 专有工具也不给子代理
|
||||
...(cfg.experimental?.primary_tools?.map(p => ({ permission: p, ... })) ?? []),
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、通信格式:Tool Call / Tool Result
|
||||
|
||||
### 3.1 父→子:Task tool 参数
|
||||
|
||||
```
|
||||
{
|
||||
subagent_type: "explore" | "general" | ...,
|
||||
description: "简短描述(3-5词)",
|
||||
prompt: "子代理的完整任务描述",
|
||||
task_id?: "恢复已有子 session 时使用",
|
||||
command?: "触发该调用的 CLI 命令(可选)",
|
||||
background?: true // 后台模式(需 experimental flag)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 子→父:XML 包装的纯文本(renderOutput)
|
||||
|
||||
```xml
|
||||
<task id="ses_xxxxx" state="completed">
|
||||
<summary>任务简述</summary>
|
||||
<task_result>
|
||||
子 agent 输出的完整文本内容...
|
||||
</task_result>
|
||||
</task>
|
||||
```
|
||||
|
||||
错误时:
|
||||
|
||||
```xml
|
||||
<task id="ses_xxxxx" state="error">
|
||||
<summary>任务失败</summary>
|
||||
<task_error>
|
||||
Error: 具体错误信息...
|
||||
</task_error>
|
||||
</task>
|
||||
```
|
||||
|
||||
### 3.3 传递给 LLM 的方式
|
||||
|
||||
**前台模式**:
|
||||
```
|
||||
TaskTool.execute() 返回 { output: "<task>...</task>" }
|
||||
↓
|
||||
AI SDK 将其转为 tool result,存入数据库 tool part
|
||||
↓
|
||||
下一轮 LLM 调用时,tool result 作为消息历史的一部分传入
|
||||
↓
|
||||
LLM 看到 XML,自行解析使用
|
||||
```
|
||||
|
||||
**后台模式**:
|
||||
```
|
||||
TaskTool.execute() 立即返回 <task state="running">...
|
||||
↓
|
||||
子 agent 完成后 → background.wait() 触发 → inject()
|
||||
↓
|
||||
向父 session 注入合成 text part(synthetic: true)
|
||||
携带 <task state="completed">... 结果
|
||||
↓
|
||||
父 LLM 在下一轮循环中看到该消息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、分发与合并(Distribution & Merge)
|
||||
|
||||
### 4.1 并行分发
|
||||
|
||||
- **无专用分发层**。依赖 LLM 在单条消息中发出多个 tool call
|
||||
- `task.txt` 引导 LLM:*"Launch multiple agents concurrently whenever possible"*
|
||||
- 底层通过 Effect.ts 的 `Effect.forkIn(scope, { startImmediately: true })` 实现同一消息内多 tool call 并发
|
||||
- **子 agent 之间完全隔离**,无直接通信
|
||||
|
||||
### 4.2 结果合并
|
||||
|
||||
**无专用合并逻辑。** 合并完全通过 LLM 的上下文理解完成:
|
||||
|
||||
- 前台:tool result 自然进入消息历史,LLM 下一轮读取
|
||||
- CLI 命令:额外注入 "Summarize the task tool output above and continue with your task." 引导 LLM 总结
|
||||
- LLM 自主调用:无额外引导,LLM 自行决定如何使用
|
||||
|
||||
### 4.3 前台/后台切换机制(task.ts lines 303-333)
|
||||
|
||||
```typescript
|
||||
// 前台执行
|
||||
return yield* Effect.raceFirst(
|
||||
background.wait({ id: nextSession.id }), // 等完成
|
||||
background.waitForPromotion(nextSession.id), // 等 promote 到后台
|
||||
)
|
||||
```
|
||||
|
||||
当用户将前台任务 promote 到后台时,`waitForPromotion` 先返回(标记 `metadata.background = true`),TaskTool 转而返回后台模式的输出。
|
||||
|
||||
### 4.4 后台作业引擎(core/background-job.ts)
|
||||
|
||||
纯内存、非持久化注册表。使用 Effect.ts 的 `SynchronizedRef` 做并发控制。
|
||||
|
||||
| 操作 | 行为 |
|
||||
|------|------|
|
||||
| `start()` | 创建 job,fork run effect,返回 info |
|
||||
| `extend()` | 追加顺序执行的 run(通过 `Deferred` 链式等待前一个完成) |
|
||||
| `wait()` | `Deferred.await(done)`,可选 timeout |
|
||||
| `waitForPromotion()` | 等待 `promoted` Deferred 或检测 `background` 标记 |
|
||||
| `promote()` | 标记 `background = true`,触发 `onPromote` callback |
|
||||
| `cancel()` | 设置 `cancelled`,close scope(中断所有子 fork) |
|
||||
|
||||
---
|
||||
|
||||
## 五、工作流推进(Workflow Progression)
|
||||
|
||||
### 5.1 核心循环(prompt.ts → runLoop)
|
||||
|
||||
```
|
||||
runLoop(sessionID):
|
||||
while true:
|
||||
1. MessageV2.filterCompactedEffect() 获取消息
|
||||
2. MessageV2.latest() 取最近 user/assistant/tasks
|
||||
3. 检查 finish 状态
|
||||
- 不是 tool-calls 且有 finish → break(退出循环)
|
||||
4. 取 tasks(subtask / compaction 队列)
|
||||
- subtask → handleSubtask() → continue
|
||||
- compaction → compaction.process() → continue/break
|
||||
5. 检查 overflow → 自动创建 compaction task → continue
|
||||
6. 构建 assistant message
|
||||
7. SessionProcessor.create() 创建 handle
|
||||
8. SessionTools.resolve() 解析所有工具
|
||||
9. 构建 system prompt(环境信息 + skills + MCP + instructions)
|
||||
10. handle.process() — 启动 LLM stream
|
||||
11. 检查 result:
|
||||
- "compact" → 返回给外层触发 compaction
|
||||
- "stop" → break
|
||||
- "continue" → 继续循环
|
||||
```
|
||||
|
||||
### 5.2 SessionProcessor 事件处理(processor.ts)
|
||||
|
||||
| Stream 事件 | 处理逻辑 |
|
||||
|------------|---------|
|
||||
| `reasoning-start/delta/end` | 创建 reasoning part → 增量追加 → 最终持久化 |
|
||||
| `tool-input-start/delta/end` | 创建/更新 tool part(pending 状态) |
|
||||
| `tool-call` | 标记 running → 设置 input → **doom loop 检测** |
|
||||
| `tool-result` | `completeToolCall()` → 持久化结果 + 附件 |
|
||||
| `tool-error` | `failToolCall()` → 标记错误 |
|
||||
| `provider-error` | 抛出异常 → 触发重试 |
|
||||
| `text-start/delta/end` | 流式文本 → `updatePartDelta()` **增量持久化** |
|
||||
| `step-start` | 创建快照(snapshot) |
|
||||
| `step-finish` | 生成 patch diff → 更新 usage/tokens → **overflow 检测** → 触发 summary |
|
||||
| `finish` | stream 结束 |
|
||||
|
||||
### 5.3 Doom Loop 检测(processor.ts lines 351-377)
|
||||
|
||||
连续 3 次**完全相同的 tool call**(相同名称 + 相同输入)触发权限询问:
|
||||
|
||||
```typescript
|
||||
const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD) // DOOM_LOOP_THRESHOLD = 3
|
||||
if (recentParts.length === DOOM_LOOP_THRESHOLD &&
|
||||
recentParts.every(part =>
|
||||
part.type === "tool" &&
|
||||
part.tool === value.name &&
|
||||
part.state.status !== "pending" &&
|
||||
JSON.stringify(part.state.input) === JSON.stringify(input)
|
||||
)) {
|
||||
yield* permission.ask({ permission: "doom_loop", ... })
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Compaction 工作流
|
||||
|
||||
两种触发方式:
|
||||
|
||||
| 触发条件 | 行为 |
|
||||
|---------|------|
|
||||
| step-finish 检测到 `isOverflow()` + `auto: true` | 创建 compaction task → 下一轮循环执行 → 压缩后 continue |
|
||||
| step-finish 检测到 `isOverflow()` + `auto: false` | 标记 `assistantMessage.error` → idle 等待用户干预 |
|
||||
|
||||
Compaction 使用专门的 `compaction` agent(hidden, mode=primary, `*=deny`)执行。
|
||||
压缩后的消息标记 `compacted: true`,后续通过 `MessageV2.filterCompactedEffect()` 过滤。
|
||||
|
||||
### 5.5 重试机制(processor.ts lines 658-672)
|
||||
|
||||
```typescript
|
||||
Effect.retry(
|
||||
SessionRetry.policy({
|
||||
provider: input.model.providerID,
|
||||
parse, // 错误解析(区分可重试/不可重试)
|
||||
set: (info) => status.set(sessionID, { type: "retry", ... }),
|
||||
}),
|
||||
)
|
||||
```
|
||||
|
||||
遇 provider 错误自动重试,LLM stream 完成后 `Effect.ensuring(cleanup)` 保证资源释放。
|
||||
|
||||
---
|
||||
|
||||
## 六、六种内置 Agent
|
||||
|
||||
| 名称 | Mode | Hidden | 用途 | 核心权限特征 |
|
||||
|------|------|--------|------|-------------|
|
||||
| `build` | primary | 否 | 默认 agent,全部工具 | question/plan_enter=allow |
|
||||
| `plan` | primary | 否 | 计划模式,禁用编辑 | edit=deny(除 plans), task(general)=deny |
|
||||
| `general` | subagent | 否 | 通用子代理 | todowrite=deny(默认禁止改 todo) |
|
||||
| `explore` | subagent | 否 | 只读代码探索 | `*=deny`,仅 read/grep/glob/bash/webfetch/websearch |
|
||||
| `compaction` | primary | 是 | 会话压缩(自动) | `*=deny` |
|
||||
| `title` | primary | 是 | 生成会话标题 | `*=deny`(step=1 时异步 fork) |
|
||||
| `summary` | primary | 是 | 生成消息摘要 | `*=deny`(每个 step-finish 时异步 fork) |
|
||||
|
||||
用户可通过 `config.agent` 自定义 agent(支持 `mode: "all"`),也可通过 `agent.generate` 让 LLM 辅助生成。
|
||||
|
||||
---
|
||||
|
||||
## 七、权限模型总结
|
||||
|
||||
```
|
||||
父 session permission
|
||||
│
|
||||
├── 仅继承 deny 规则 + external_directory 规则 ← subagent-permissions.ts
|
||||
│ (父 agent 的 allow 规则不传播到子代理)
|
||||
│
|
||||
├── 子代理自身 permission(来自 agent 定义)
|
||||
│
|
||||
├── 默认 deny:
|
||||
│ - todowrite(除非子代理明确允许)
|
||||
│ - task(除非子代理明确允许,默认防嵌套)
|
||||
│
|
||||
└── 主 agent 专有工具 deny(来自 config.experimental.primary_tools)
|
||||
```
|
||||
|
||||
子代理的 session 权限 = **父 deny + 父 external_directory + 自身 permission - 默认 deny - primary_tools deny**。
|
||||
|
||||
---
|
||||
|
||||
## 八、关键设计决策
|
||||
|
||||
| 决策 | 意图 | 效果/局限 |
|
||||
|------|------|----------|
|
||||
| 结果以 XML 纯文本嵌入上下文 | 简单、LLM 可直接理解 | LLM 自行解析 XML;大结果可能被截断 |
|
||||
| 无专用 merge 逻辑 | 简洁,不引入额外抽象 | 依赖 LLM 的理解能力处理返回结果 |
|
||||
| 默认禁止子代理嵌套 task | 防止无限递归 | 限制了多级分解场景 |
|
||||
| 同一消息多 tool call 并发 | 利用 LLM 并行能力 | 子 agent 隔离,无法协作 |
|
||||
| Effect.ts 贯穿全程 | 类型安全、结构化并发 | 学习曲线陡峭 |
|
||||
| session 作为隔离边界 | 天然权限/消息隔离 | 每个子 session 独立数据库记录,开销较大 |
|
||||
| 后台引擎纯内存 | 有意识取舍(注释说明) | 进程重启丢失状态 |
|
||||
|
||||
---
|
||||
|
||||
## 九、参考源码路径
|
||||
|
||||
| 文件 | 角色 |
|
||||
|------|------|
|
||||
| `packages/opencode/src/tool/task.ts` | Task tool 核心实现(调度入口) |
|
||||
| `packages/opencode/src/tool/task.txt` | Task tool 的 LLM 使用说明 |
|
||||
| `packages/opencode/src/agent/agent.ts` | Agent 定义注册中心 |
|
||||
| `packages/opencode/src/agent/subagent-permissions.ts` | 子代理权限推导 |
|
||||
| `packages/opencode/src/tool/registry.ts` | 工具注册 + `describeTask()` 列出可用子代理 |
|
||||
| `packages/opencode/src/session/prompt.ts` | 会话循环 + `handleSubtask()` + 提示词构建 |
|
||||
| `packages/opencode/src/session/processor.ts` | LLM stream 事件处理器 |
|
||||
| `packages/opencode/src/session/tools.ts` | Tool ↔ AI SDK 桥接 |
|
||||
| `packages/opencode/src/session/system.ts` | 系统提示词生成(含 Task tool 说明) |
|
||||
| `packages/opencode/src/background/job.ts` | 后台作业包装层 |
|
||||
| `packages/core/src/background-job.ts` | 后台作业核心引擎(内存注册表) |
|
||||
+292
-63
@@ -1,13 +1,13 @@
|
||||
# AG Core Roadmap
|
||||
|
||||
> 定稿日期:2026-05-11
|
||||
> 最后更新:2026-07-04(v0.1 发布完成)
|
||||
> 最后更新:2026-07-04
|
||||
|
||||
## 愿景
|
||||
|
||||
AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可插拔的架构,提供大模型调用、提示词工程、工具系统、记忆检索四大核心能力,支持快速组合出符合业务需求的智能体应用。
|
||||
|
||||
**当前状态**:Phase 0-4c 全部完成;Provider IR 重构(统一类型系统 + OpenAI/Anthropic/DeepSeek/Qwen Provider)已完成;LlmCycle 简化(IR 消息类型切换 + 桥接层移除)已完成;v0.1 发布就绪(**182 个测试通过、0 clippy 警告、7 个离线示例可运行**)。
|
||||
**当前状态**:v0.1.0 已发布(2026-07-04)。Phase 0-4c 全部完成,Provider IR 重构 + LlmCycle 简化 + 7 个离线示例已交付。v0.2.0 已细分为 8 个增量 Phase(Phase 5-12),本周开发启动。
|
||||
|
||||
---
|
||||
|
||||
@@ -240,95 +240,323 @@ graph BT
|
||||
|
||||
---
|
||||
|
||||
## 扩展计划(v0.2+)
|
||||
## v0.2.0 — 生产就绪(Production-Ready Core)
|
||||
|
||||
> 以下功能在已完成的 phase 中已实现基础能力或在 Phase 4 阶段明确了边界,后续可按维度增量扩展。
|
||||
> 设计参考:见 `docs/note-agent-harness-references.md`(OpenClaw / Hermes / OpenHuman / OpenHarness 横向对比)。
|
||||
> OpenCode 借鉴:见 `docs/note-opencode-agent-switching.md`(Agent 切换 + System Prompt 拼接机制)。
|
||||
**目标**:解决 Rust Agent 工具箱从"能跑"到"能被人依赖"的鸿沟。持久化、配置层、上下文管理三大块补齐后,开发者可在 30 分钟内写出生产可用的 Agent 服务。
|
||||
|
||||
### 已有扩展项(沿用)
|
||||
**总体规模**:8 个增量 Phase(Phase 5-12),17 个可验证 Step。
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| Prompt Optimizer | `prompt` | 提示词自动优化 | P3 | 待实现 |
|
||||
| 流式接口优化 | `llm/stream` | 流式响应解析与事件化 | P0 | ✅ 已完成基础实现 |
|
||||
### 功能清单
|
||||
|
||||
### v0.2+ 新增扩展项
|
||||
#### P0 — 必须交付
|
||||
|
||||
> 以下为基于 Phase 4 设计讨论确定的 v0.2+ 候选扩展方向,按维度分组。
|
||||
> 标注为"v0.2 待评估"表示在 Phase 4 完成后再决定是否启动。
|
||||
| # | 功能 | 模块 | 方案要点 |
|
||||
|---|------|------|---------|
|
||||
| 1 | SqliteStore | `memory` | `rusqlite` + `bundled` feature,`MemoryStore` 的 SQLite 实现,进程重启数据不丢 |
|
||||
| 2 | ProviderConfig 扩展 + `from_env()` | `llm` | 补全 `timeout_secs` / `max_retries` 字段;`AG_LLM_*` 环境变量辅助函数 |
|
||||
| 3 | ToolDefinition IR 正式化 | `tools` | 移除 deprecated OpenAI wire 格式,替换为自定义 `ToolDef` 结构体 |
|
||||
| 4 | API 稳定性管理 | `*` | 公开枚举加 `#[non_exhaustive]`;CHANGELOG 记录 Breaking Changes;废弃 API 用 `#[deprecated]` 标记 |
|
||||
| 5 | Quick Start + 端到端示例 | `examples/` | 30 行 `main.rs` 快速开始;一个"SQLite 持久化 + Provider + 工具调用 + 多轮对话"的可运行示例(`cargo run --example`) |
|
||||
|
||||
#### Multi-Agent / 协同
|
||||
#### P1 — 重要但不阻塞
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| Multi-Agent 协同(Swarm) | `agent` | 子 Agent 委派、并行子任务、结果聚合 | P2 | v0.2 待评估 |
|
||||
| # | 功能 | 模块 | 方案要点 |
|
||||
|---|------|------|---------|
|
||||
| 6 | Ollama Provider | `llm/provider` | OpenAI Compat,本地 LLM 支持,实现量极小 |
|
||||
| 7 | VectorRetriever trait | `memory` | 语义检索 trait 抽象(`index` / `search`),不绑定后端实现 |
|
||||
| 8 | 流式 `submit_turn_stream` | `agent` | `AgentSession` 新增 `submit_turn_stream()`,返回 `Stream<Item = StreamEvent>` |
|
||||
| 9 | 测试补强 | `*` | wiremock Provider roundtrip 测试;多线程并发写入 MemoryStore 测试 |
|
||||
|
||||
#### 技能(Skills)
|
||||
#### P2 — 有时间再做
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| Markdown 技能按需加载 | `agent` / `prompt` | 兼容 `SKILL.md` 格式(Hermes / OpenHarness 风格),按 prompt 上下文动态加载 | P2 | v0.2 待评估 |
|
||||
| # | 功能 | 模块 | 备注 |
|
||||
|---|------|------|------|
|
||||
| 10 | MCP StreamableHttp | `tools` | 当前仅预留枚举变体 |
|
||||
| 11 | Gemini Provider | `llm/provider` | 协议差异大,实现成本较高 |
|
||||
| 12 | 文件系统 MemoryStore 后端 | `memory` | JSON/JSONL 轻量持久化 |
|
||||
|
||||
#### 记忆(Memory)
|
||||
### ContextSlot 上下文管理
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| 多通道检索(hybrid) | `memory/retriever` | 在 TextOverlap 之上叠加向量检索通道 | P2 | v0.2 待评估 |
|
||||
| KnowledgeGraph 深度记忆 | `memory` | 实体-关系图、`note-knowledge-graph-design.md` 已记录设计 | P3 | v0.2 待评估 |
|
||||
| TokenJuice 智能压缩 | `memory` / `llm/compact` | 借鉴 OpenHuman TokenJuice,对工具结果做语义压缩而非字节截断 | P3 | v0.2 待评估 |
|
||||
**模块归属**:`src/llm/context.rs`(与 `compact.rs` 同级)
|
||||
|
||||
#### 交互层(TUI / Gateway)
|
||||
**核心概念**:`ContextSlot` 是一段带策略配置的消息列表,以 `slot_id` 为 namespace 独立持久化到 `MemoryStore`。支持三种模式、三种来源和派生关联(记录 `parent_id`)。
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| TUI / 多平台 Gateway | 应用层 | OpenClaw / Hermes 风格的消息平台桥接(Feishu / Telegram / Discord 等) | P3 | v0.2+ 应用层 |
|
||||
**核心类型**:
|
||||
|
||||
#### 训练基础设施
|
||||
```rust
|
||||
pub struct ContextSlot { id, session_id, config, messages, store }
|
||||
pub struct SlotConfig { mode: SlotMode, source: SlotSource, budget, compact }
|
||||
pub enum SlotMode {
|
||||
Full, // 完整对话历史
|
||||
Focused(FocusedConfig), // 聚焦:保持 LLM 注意力
|
||||
Readonly, // 只读参考上下文
|
||||
}
|
||||
pub struct FocusedConfig { keep_system, recent_turns, inject_summary }
|
||||
pub enum SlotSource {
|
||||
New, // 全新空槽,独立持久化
|
||||
Derived { parent_id, strategy: DeriveStrategy }, // 从父 slot 派生
|
||||
Static(Vec<Message>), // 预置消息,不持久化
|
||||
}
|
||||
pub enum DeriveStrategy { Full, Focused(FocusedConfig) }
|
||||
pub struct ContextBudget { system, history, tools, tool_results, reserve }
|
||||
```
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| RL 轨迹导出 | `agent` | ShareGPT 格式轨迹、Atropos 集成(Hermes 风格) | P3 | v0.3+ 探索 |
|
||||
**持久化 Key 命名**:
|
||||
- `slot_msg:{session_id}:{slot_id}:{index}` → 消息内容
|
||||
- `slot_meta:{session_id}:{slot_id}` → `SlotMeta`(含 `parent_id`)
|
||||
- `slot_rel:{session_id}:{child_id}:parent` → `"{parent_id}"`
|
||||
|
||||
#### 安全治理
|
||||
**`AgentSession` 扩展**:
|
||||
- `create_slot(id, config)` — 创建新 slot
|
||||
- `switch_slot(id)` — 切换当前 slot
|
||||
- `list_slots()` — 列出所有 slot
|
||||
- `derive_slot(id, parent_id, strategy)` — 从父 slot 派生
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| Human-in-the-loop 审批 | `agent` / `tools/permission` | 高危工具执行前的异步审批回调(OpenHarness `permission_prompt` 模式) | P2 | v0.2 待评估 |
|
||||
**与 `ConversationMemory` 的关系**:保留不废除。`ConversationMemory` 继续服务传统对话场景。
|
||||
|
||||
#### 流式 / 实时
|
||||
**v0.2 不做**:
|
||||
- ❌ `slot.fork()` / `merge()` — 分支方法推迟到 v0.3+
|
||||
- ❌ `inject_summary` 自动生成 — v0.2 仅消费端(从 `SessionMemory` 读取),生成在 v0.3+
|
||||
- ❌ 血缘关系图遍历 — 只存 `parent_id`,不做查询
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| 流式 `submit_turn` | `agent/session` | Phase 4 v1 只暴露非流式 `submit_turn()`;v0.2 包装 `LlmCycle::submit_stream` 暴露流式入口 | P2 | v0.2 待评估 |
|
||||
**依赖**:Phase 0(MemoryStore trait)、Phase 3(MemoryStore 持久化)
|
||||
**优先级**:P1
|
||||
|
||||
#### Agent 切换 / Prompt 动态(OpenCode 借鉴)
|
||||
---
|
||||
|
||||
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|
||||
|-------|---------|------|--------|------|
|
||||
| Agent 身份切换(角色轮换) | `agent` | 借鉴 OpenCode Tab 键切换 build/plan:同一 `AgentSession` 持有可热替换的 `Agent` 引用,切换时不重置消息历史,在末尾追加 `synthetic: true` 的状态变更消息。详见 `docs/note-opencode-agent-switching.md` §4 | P2 | v0.2 待评估 |
|
||||
| System Prompt 多层动态拼接 | `agent/session` | 借鉴 OpenCode `request.ts:58-66`:拆分 `base_prompt + agent_prompt + env_context` 三层,`AgentSession::submit_turn` 每轮重算(不缓存),便于按 agent 类型动态切换 | P2 | v0.2 待评估 |
|
||||
| **多 Context 切换** | `agent` | **Phase 4c 的 SessionMemory 数据结构已预留信息桥接通道,v0.2+ 在其上包装 `ContextManager` 实现完整的多 context 切换:创建/销毁/切换 context、通过 SessionMemory 桥接关键信息。详见 `docs/note-context-switch-design.md`** | P2 | v0.2 待评估 |
|
||||
### v0.2.0 实施计划 — 8 个增量 Phase
|
||||
|
||||
> **编号说明**:Phase 5-12 接续 v0.1 的 Phase 0-4c,按开发顺序排列。
|
||||
|
||||
#### Phase 5: 热身准备(Warmup)
|
||||
|
||||
**目标**:快速交付三个互不依赖的独立改动,建立交付节奏。
|
||||
|
||||
| Step | 内容 | 文件范围 | 验证标准 |
|
||||
|------|------|---------|---------|
|
||||
| **5.1** | `ProviderConfig` 扩展:补 `timeout_secs`(def=30) + `max_retries`(def=3);新增 `ProviderConfig::from_env(prefix)` | `llm/provider.rs` + 各 Provider `new()` 构造函数 | `cargo test` + `from_env()` 单元测试 |
|
||||
| **5.2** | `OllamaProvider`:基于 `GenericOpenaiProvider` 包装,改 base_url 为 `http://localhost:11434`;`ProviderType` 新增 `Ollama` | `llm/provider/provider.rs` + `llm/provider/ollama.rs`(新增) | `cargo build` — 纯类型级验证 |
|
||||
| **5.3** | 公开枚举 `#[non_exhaustive]` 前置标记:`ProviderType` / `StopReason` / `FinishReason` / `EvictionPolicy` / `SlotMode`(预置) | 各枚举定义处 | 编译通过 + `cargo clippy` 0 警告 |
|
||||
|
||||
**依赖**:无(三个 Step 互不冲突)
|
||||
**优先级**:P0(5.1)+ P1(5.2)+ P0 前置(5.3)
|
||||
**为何独立成 Phase**:三个改动零文件重叠,可以并行推进。它们是后续所有 Phase 的"门把手"——先做完热身再进入核心工作。
|
||||
|
||||
---
|
||||
|
||||
#### Phase 6: ToolDefinition IR 正式化
|
||||
|
||||
**目标**:引入 `ToolDef` 新类型,替换已标记 `#[deprecated]` 的 `ToolDefinition`(`OpenaiToolDefinition` 别名)。
|
||||
|
||||
**这是 v0.2 技术风险最高的 Phase**,影响 4 个模块约 8 个文件。通过 5 个 Step 逐文件切割确保每步可编译。
|
||||
|
||||
| Step | 内容 | 验证标准 |
|
||||
|------|------|---------|
|
||||
| **6.1** | `types/tool.rs` 新增 `ToolDef` 结构体 + `From<ToolDef> for OpenaiToolDefinition` + 反向 `From` | 单元测试 roundtrip |
|
||||
| **6.2** | `types/mod.rs` 切别名 `pub type ToolDefinition = ToolDef`;`MessageRequest.tools` 改 `Vec<ToolDef>` | `cargo build` 编译断点 |
|
||||
| **6.3** | `cycle.rs` 4 个方法签名 + `registry.rs` `definitions()` 签名更新 | `cargo build` |
|
||||
| **6.4** | Provider 适配层(openai.rs / anthropic.rs / openai_compat.rs):`build_request()` 内做 `ToolDef → wire-format` 转换 | `cargo test` 每个 provider 测试 |
|
||||
| **6.5** | 所有测试/示例中 `ToolDefinition` → `ToolDef` 修复;移除旧 `#[deprecated]` alias | `cargo test --all-targets` 全绿 |
|
||||
|
||||
**边界切割技巧**:
|
||||
- Step 6.1 → 6.2 之间是安全 checkpoint:新类型存在但旧代码照常编译
|
||||
- Provider 层不改序列化逻辑,只加一层 `From` 转换
|
||||
- 当前代码中 `ToolDefinition` 已是 `#[deprecated(since = "0.1.0")]`,用户已有迁移预期
|
||||
|
||||
**依赖**:无(仅与 Phase 5.3 有枚举兼容关系)
|
||||
**优先级**:P0
|
||||
|
||||
---
|
||||
|
||||
#### Phase 7: SqliteStore 持久化
|
||||
|
||||
**目标**:实现 `MemoryStore` 的 SQLite 后端,进程重启数据不丢。
|
||||
|
||||
**与 Phase 6 无耦合,可重叠开发。**
|
||||
|
||||
| Step | 内容 | 文件 | 验证标准 |
|
||||
|------|------|-----|---------|
|
||||
| **7.1** | 新增 `memory/store/sqlite.rs`:`Mutex<Connection>` + `spawn_blocking`,实现 `save/get/delete/list` + prefix 过滤 | `memory/store/sqlite.rs` + `Cargo.toml`(add `rusqlite`) | 单元测试 CRUD + prefix 查询 |
|
||||
| **7.2** | WAL 模式 + 并发安全 + 集成测试(`tokio::spawn` 10 个并发 task) | `sqlite.rs` 扩展 | 并发写入 100 轮无 race |
|
||||
|
||||
**设计决策**:
|
||||
- 用 `Mutex<Connection>` 而非连接池(ponytail:一个连接够用就不加 r2d2)
|
||||
- WAL 模式:`PRAGMA journal_mode=WAL` 解决读写锁
|
||||
|
||||
**依赖**:`MemoryStore` trait(v0.1 Phase 3 已就绪)
|
||||
**优先级**:P0
|
||||
|
||||
---
|
||||
|
||||
#### Phase 8: MVP 集成出口(v0.2.0-rc.1 候选)
|
||||
|
||||
**目标**:P0 五项全部交付。开发者 clone 仓库后 10 分钟跑起持久化 Agent。
|
||||
|
||||
| Step | 内容 | 验证标准 |
|
||||
|------|------|---------|
|
||||
| **8.1** | API 稳定性扫尾:`#[deprecated]` 整理 + CHANGELOG v0.2 + 公开类型回顾 | 人工 review + `cargo doc` 无 warning |
|
||||
| **8.2** | Quick Start 示例(30 行 `main.rs`):MockProvider + EchoTool + 一次 `submit_turn` | `cargo run --example quick_start` exit 0 |
|
||||
| **8.3** | 端到端示例:SqliteStore + Ollama/OpenAI(from_env) + 自定义 Tool + 多轮对话 | `cargo run --example end_to_end`(Mock fallback,无需 API key)|
|
||||
|
||||
**Phase 8 完成后可打 `v0.2.0-rc.1` 标签**。
|
||||
|
||||
**依赖**:Phase 5(ProviderConfig from_env)+ Phase 6(ToolDef)+ Phase 7(SqliteStore)
|
||||
**优先级**:P0
|
||||
|
||||
---
|
||||
|
||||
#### Phase 9: 流式体验增强
|
||||
|
||||
**目标**:Agent 会话支持流式输出,开发者看到实时 token。
|
||||
|
||||
| Step | 内容 | 文件 | 验证标准 |
|
||||
|------|------|-----|---------|
|
||||
| **9.1** | `AgentSession::submit_turn_stream(user_input) -> impl Stream<Item=StreamEvent>` | `agent/session.rs` | 单元测试验证流事件序列:`TextDelta → ... → MessageComplete` |
|
||||
|
||||
**注意**:tool 自动循环时流中插入 `ToolExecutionStarted` 事件,用户端 UI 显示"正在调用工具..."。
|
||||
|
||||
**依赖**:Phase 6(ToolDef)+ `LlmProvider.chat_stream`(v0.1 已有)
|
||||
**优先级**:P1
|
||||
|
||||
---
|
||||
|
||||
#### Phase 10: ContextSlot 上下文管理
|
||||
|
||||
**目标**:支持多上下文分区管理,Agent 可在不同 slot 之间切换。
|
||||
|
||||
| Step | 内容 | 验证标准 |
|
||||
|------|------|---------|
|
||||
| **10.1** | `src/llm/context.rs`:`ContextSlot` + `SlotConfig` / `SlotMode` / `SlotSource` / `ContextBudget` 核心类型 | `cargo build` |
|
||||
| **10.2** | ContextSlot 持久化:基于 `MemoryStore` trait(不绑定 SqliteStore)实现 save/load/list + slot 命名空间 key 策略 | 单元测试:slot 创建/写入/读取/隔离(不串数据) |
|
||||
| **10.3** | `AgentSession` 扩展:`create_slot` / `switch_slot` / `list_slots` / `derive_slot` + `AgentBuilder` 默认创建 `"default"` slot | 集成测试 + 新示例 `context_slot_demo` |
|
||||
|
||||
**如何保证简单场景无感**:`AgentBuilder::build()` 内部检查,如果用户没手动 `create_slot`,自动创建 `"default"` slot → `submit_turn` 默认写到 default slot。
|
||||
|
||||
**依赖**:Phase 7(SqliteStore 作为推荐持久化后端;`MemoryStore` trait 即可)
|
||||
**优先级**:P1
|
||||
|
||||
---
|
||||
|
||||
#### Phase 11: 测试与检索补强
|
||||
|
||||
**目标**:补全测试覆盖 + 语义检索抽象。
|
||||
|
||||
| Step | 内容 | 验证标准 |
|
||||
|------|------|---------|
|
||||
| **11.1** | `VectorRetriever` trait:`index(id, embeddings)` + `search(query, k)` | 编译 + mock 测试 |
|
||||
| **11.2** | wiremock Provider roundtrip 测试:模拟 OpenAI/Anthropic HTTP 端点 | `cargo test` 新增 10+ roundtrip 测试 |
|
||||
| **11.3** | 并发测试补强:InMemoryStore + SqliteStore 多线程写入验证 | 跑 100 轮无 race |
|
||||
|
||||
**依赖**:无(可随时做)
|
||||
**优先级**:P1
|
||||
|
||||
---
|
||||
|
||||
#### Phase 12: P2 锦上添花(可选)
|
||||
|
||||
**目标**:时间允许时按优先级交付。
|
||||
|
||||
| 优先级 | 功能 | 实现量估计 | 备注 |
|
||||
|--------|------|-----------|------|
|
||||
| **12.1** | 文件系统 MemoryStore(JSON/JSONL) | ~80 行 | 最简单,适合练手 |
|
||||
| **12.2** | MCP StreamableHttp 传输 | ~150 行 | 协议还在演进 |
|
||||
| **12.3** | Gemini Provider | ~300 行 | 协议差异大,建议推迟到 v0.3 |
|
||||
|
||||
**依赖**:无(独立交付)
|
||||
|
||||
---
|
||||
|
||||
### v0.2.0 Phase 依赖关系图
|
||||
|
||||
```mermaid
|
||||
graph BT
|
||||
P5["Phase 5<br/>热身准备"]:::warmup
|
||||
P6["Phase 6<br/>ToolDefinition IR"]:::core
|
||||
P7["Phase 7<br/>SqliteStore"]:::core
|
||||
P8["Phase 8<br/>MVP 出口 (rc.1)"]:::mvp
|
||||
P9["Phase 9<br/>流式体验增强"]:::p1
|
||||
P10["Phase 10<br/>ContextSlot"]:::p1
|
||||
P11["Phase 11<br/>测试与检索"]:::p1
|
||||
P12["Phase 12<br/>P2 锦上添花"]:::p2
|
||||
|
||||
P8 --> P5
|
||||
P8 --> P6
|
||||
P8 --> P7
|
||||
|
||||
P9 --> P6
|
||||
|
||||
P10 --> P7
|
||||
P10 --> P8
|
||||
|
||||
P11 -.-> P7
|
||||
|
||||
classDef warmup fill:#e2e8f0,stroke:#94a3b8
|
||||
classDef core fill:#fbbf24,stroke:#d97706
|
||||
classDef mvp fill:#4ade80,stroke:#16a34a
|
||||
classDef p1 fill:#93c5fd,stroke:#2563eb
|
||||
classDef p2 fill:#c4b5fd,stroke:#7c3aed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 关键里程碑
|
||||
|
||||
| 里程碑 | Phase 完成条件 | 可验证指标 |
|
||||
|--------|---------------|-----------|
|
||||
| **M1** | Phase 5 | 热身三项完成:`from_env()` 可用 / Ollama 类型存在 / `#[non_exhaustive]` 就位 |
|
||||
| **M2** | Phase 6 | `ToolDef` 全量切换,`cargo test --all-targets` 全绿 |
|
||||
| **M3** | Phase 7 | SqliteStore CRUD + 并发测试通过,进程重启数据不丢 |
|
||||
| **M4** | **Phase 8 (rc.1)** | P0 五项全部交付,`cargo run --example quick_start` 跑通 |
|
||||
| **M5** | Phase 9 | `submit_turn_stream` 流式事件序列验证通过 |
|
||||
| **M6** | Phase 10 | ContextSlot 创建/切换/派生集成测试通过 |
|
||||
| **M7** | Phase 11 | wiremock + 并发测试补强,测试总量 200+ |
|
||||
| **M8** | Phase 12(可选) | P2 功能按需交付 |
|
||||
|
||||
---
|
||||
|
||||
## v0.3+ 展望
|
||||
|
||||
### 已规划的功能
|
||||
|
||||
| 功能 | 说明 | 预计版本 |
|
||||
|------|------|---------|
|
||||
| ContextSlot 分支(fork/merge) | 在决策点 fork 出子上下文,分支独立演进,可合并/丢弃 | v0.3 |
|
||||
| 摘要自动生成 | Hook 驱动,`OnTurnEnd` 自动将对话摘要写入 `SessionMemory`,`inject_summary` 消费端已在 v0.2 就绪 | v0.3 |
|
||||
| 知识图谱 | 实体-关系图,`docs/note-knowledge-graph-design.md` 已记录设计 | v0.3+ |
|
||||
| Multi-Agent 协同(Swarm) | 子 Agent 委派、并行子任务、结果聚合 | v0.4+ |
|
||||
| 精确 tokenizer 计数 | 绑定具体模型的 tokenizer 计数,替代当前的字符估算 | v0.3+ |
|
||||
| 血缘关系图遍历 | 以 `parent_id` 为基础,提供 slot 血缘链查询 | v0.3+ |
|
||||
| Markdown 技能按需加载 | 兼容 `SKILL.md` 格式,按 prompt 上下文动态加载 | v0.3+ |
|
||||
| TokenJuice 语义压缩 | 对工具结果做语义压缩而非字节截断 | v0.3+ |
|
||||
| Human-in-the-loop 审批 | 高危工具执行前的异步审批回调 | v0.3+ |
|
||||
| RL 轨迹导出 | ShareGPT 格式轨迹、Atropos 集成 | v0.4+ |
|
||||
|
||||
### 明确不做(agcore 范围外)
|
||||
|
||||
| 功能 | 原因 |
|
||||
|------|------|
|
||||
| TUI / 多平台 Gateway | 应用层职责(Feishu / Telegram / Discord 桥接) |
|
||||
| 配置自动加载(config/figment) | 配置来源策略应由上游应用决定,agcore 不定义配置格式 |
|
||||
| 提示词自动优化 | 属于智能层,不应内建于 core 库 |
|
||||
|
||||
---
|
||||
|
||||
## 风险与建议
|
||||
|
||||
1. **Phase 0 已完成**:LLM 调用周期基础设施已全部实现,可以支撑后续模块开发
|
||||
2. **并行可能性**:Phase 0 和 Phase 1 可并行开展(无相互依赖),可加速早期交付
|
||||
3. **MCP 协议复杂性**:MCP 涉及协议握手、session 管理、长期连接,建议预留充足时间调研协议细节
|
||||
4. **Scope 蔓延风险**:当前 specs 只有 1 份文档,建议每个模块上线前都产出对应 spec,避免边实现边设计
|
||||
5. **Phase 4 抽象化边界**:AG Core 定位为"支持库"而非"Agent 产品",Phase 4(4a/4b/4c)需严格控制范围——只暴露 trait + 最小 reference impl,业务循环(多轮 turn 编排、对话记忆自动回写、Task 拆解策略)留给上层应用。`SessionMemory`(Phase 4c)提供信息桥接通道但不实现 context 切换逻辑。多 context 切换管理延后至 v0.2+。详细设计决策见 `docs/7-agent-runtime.md`
|
||||
6. **参考项目语言差异**:OpenClaw / Hermes / OpenHarness 均为 Python/TypeScript 实现,OpenHuman 虽是 Rust + Tauri 但定位是桌面应用。借鉴时**只取架构模式**,不照搬具体实现(如 Pydantic 工具校验、SQLite Memory Tree、Node+Python 双进程等)
|
||||
1. **持久化依赖**:`rusqlite` + `bundled` 零外部依赖编译,但 SQLite 不适配所有场景(分布式/高并发写)。`MemoryStore` trait 的抽象层允许下游自行实现 Redis / PostgreSQL 后端
|
||||
2. **ContextSlot 心智负担**:`ContextSlot` 引入了一等抽象的复杂度。建议通过 `AgentBuilder` 默认创建 `"default"` slot,让简单场景无感使用
|
||||
3. **向量检索生态**:`VectorRetriever` trait-only 不绑定实现,需社区贡献或用户自行适配 pgvector / qdrant / lancedb
|
||||
4. **Scope 蔓延**:agcore 定位为"支持库"而非"Agent 产品",始终以 trait + reference impl 为边界,业务循环留给上层
|
||||
5. **API 稳定性**:v0.2 引入 `#[non_exhaustive]` 和 `#[deprecated]` 机制,但不承诺 SemVer 稳定——仍在快速迭代期
|
||||
|
||||
---
|
||||
|
||||
## 下一步行动
|
||||
|
||||
1. **Phase 4c 已完成**:Phase 4a + 4b + 4c 已交付(116 测试通过,0 clippy 警告)。可启动 v0.2+ 扩展评估(如多 Context 切换、Multi-Agent 协同等)
|
||||
2. **Context 切换备忘**:`docs/note-context-switch-design.md` 记录了多 context 切换方案讨论,作为 v0.2+ 扩展项的输入
|
||||
3. **参考项目调研沉淀**:已完成 OpenClaw / Hermes / OpenHuman / OpenHarness 横向调研,结果沉淀至 `docs/note-agent-harness-references.md`,作为 v0.2+ 扩展项的输入
|
||||
4. **Phase 3 备用设计就绪**:`docs/note-knowledge-graph-design.md` 记录了 KnowledgeGraph、高级评分、RecallBased 淘汰等设计,v0.2+ 记忆扩展可直接参考
|
||||
1. **Phase 5 启动**:ProviderConfig from_env + Ollama Provider + #[non_exhaustive] 前置,三个 Step 并行推进
|
||||
2. **Phase 6 方案准备**:ToolDef 结构体定义 + 兼容转换,出实施笔记(实施时直接走代码评审)
|
||||
3. **示例先行**:每完成一个 Phase 立即更新对应示例,验证通过后再合入
|
||||
4. **里程碑追踪**:以 Phase 8(MVP 出口)为 v0.2.0-rc.1 节点,逐 Phase 验收
|
||||
|
||||
**已完成 / 进行中阶段**:
|
||||
- ✅ Phase 0 Foundation — 全部交付物已完成
|
||||
@@ -338,9 +566,10 @@ graph BT
|
||||
- ✅ Phase 4a Core Glue — 全部交付物已完成
|
||||
- ✅ Phase 4b Task Execution — 全部交付物已完成
|
||||
- ✅ Phase 4c Session Memory — 全部交付物已完成
|
||||
- ✅ Provider IR 重构 — 统一类型系统 + OpenAI/Anthropic/DeepSeek/Qwen 适配(方案:`docs/10-llm-provider-refinement.md`、`docs/10a-phase0-types-and-trait.md`、`docs/10b-phase1-provider-adaptation.md`)
|
||||
- ✅ LlmCycle 简化 — IR 消息类型切换 + Phase 0 桥接层移除(方案:`docs/10c-phase2-llm-cycle-simplify.md`)
|
||||
- ✅ v0.1 Release — 技术债扫清、MockProvider 公开化、7 个离线示例、README + 错误消息友好化、Roadmap 同步、CHANGELOG 初始化(计划:`docs/11-v0.1-release-plan.md`)
|
||||
- ✅ Provider IR 重构 — 统一类型系统 + OpenAI/Anthropic/DeepSeek/Qwen 适配
|
||||
- ✅ LlmCycle 简化 — IR 消息类型切换 + Phase 0 桥接层移除
|
||||
- ✅ v0.1 Release — 技术债扫清、MockProvider 公开化、7 个离线示例、README + 错误消息友好化、CHANGELOG 初始化
|
||||
- 📋 **v0.2 规划细化完成** — 8 个增量 Phase(Phase 5-12),17 个可验证 Step,覆盖 P0-P2 全部 12 项功能 + ContextSlot
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user