26 KiB
Agent 配置入参功能 - 详细实现计划
基于
01-agent-config-instruction-spec.md设计文档生成
1. 实现概述
1.1 目标
为 /chat 接口增加三个可选配置入参:
system_prompt: 系统提示词user_prompt: 用户提示词模板(支持{user_prompt}变量注入)agent_config: Agent 运行时配置(包含agent_server和context_servers)
1.2 实现原则
- 向后兼容: 所有新字段可选,不影响现有调用
- 职责分离: 提示词由独立入参控制,
agent_config只负责运行时配置 - 类型安全: 使用强类型结构体,避免 JSON 字符串解析
1.3 实现分批
| 批次 | 内容 | 预计文件数 |
|---|---|---|
| 第一批 | 基础结构(类型定义、proto、请求结构) | 5 |
| 第二批 | 核心逻辑(配置组装器、gRPC 服务) | 4 |
| 第三批 | 启动器集成(ClaudeCodeLauncher) | 2 |
| 第四批 | 测试和文档 | 3+ |
2. 第一批:基础结构
2.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|---|---|---|---|
| 1.1 | 创建 ChatAgentConfig 结构体 | crates/shared_types/src/chat_agent_config.rs |
新增 |
| 1.2 | 导出新类型 | crates/shared_types/src/lib.rs |
修改 |
| 1.3 | 修改 gRPC proto 定义 | crates/shared_types/proto/agent.proto |
修改 |
| 1.4 | 修改 HTTP ChatRequest | crates/rcoder/src/handler/chat_handler.rs |
修改 |
| 1.5 | 修改 gRPC 客户端传参 | crates/rcoder/src/grpc/chat_client.rs |
修改 |
2.1.1 创建 ChatAgentConfig 结构体
文件: crates/shared_types/src/chat_agent_config.rs
步骤:
- 创建新文件
- 定义三个结构体:
ChatAgentConfig- 顶层配置ChatAgentServerConfig- Agent 服务器配置ChatContextServerConfig- MCP 服务器配置
- 实现
Defaulttrait - 实现辅助方法:
has_agent_server(),has_context_servers(),get_enabled_context_servers(),get_agent_id()
代码模板:
// 见设计文档 2.2 节完整代码
验证点:
- 编译通过
- JSON 序列化/反序列化测试通过
- 默认值正确
2.1.2 导出新类型
文件: crates/shared_types/src/lib.rs
修改内容:
// 新增
mod chat_agent_config;
pub use chat_agent_config::{ChatAgentConfig, ChatAgentServerConfig, ChatContextServerConfig};
验证点:
cargo build -p shared_types编译通过- 其他 crate 可以正常引用新类型
2.1.3 修改 gRPC proto 定义
文件: crates/shared_types/proto/agent.proto
修改内容:
- 在
ChatRequest消息中添加新字段:
message ChatRequest {
// ... 现有字段 (1-7) ...
// 新增字段
optional string system_prompt = 8; // 系统提示词
optional string user_prompt = 9; // 用户提示词模板
optional ChatAgentConfig agent_config = 10; // Agent 运行时配置
}
- 添加新消息定义:
// Agent 运行时配置
message ChatAgentConfig {
optional ChatAgentServerConfig agent_server = 1;
map<string, ChatContextServerConfig> context_servers = 2;
}
// 单个 Agent 服务器配置
message ChatAgentServerConfig {
optional string agent_id = 1;
optional string command = 2;
repeated string args = 3;
map<string, string> env = 4;
map<string, string> metadata = 5;
}
// MCP 服务器配置
message ChatContextServerConfig {
string source = 1;
bool enabled = 2;
optional string command = 3;
repeated string args = 4;
map<string, string> env = 5;
}
验证点:
cargo build -p shared_types编译通过(proto 自动生成)- 生成的 Rust 代码位于
src/grpc/agent.rs
2.1.4 修改 HTTP ChatRequest
文件: crates/rcoder/src/handler/chat_handler.rs
修改内容:
- 添加 import:
use shared_types::ChatAgentConfig;
- 在
ChatRequest结构体中添加新字段:
#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)]
pub struct ChatRequest {
// ... 现有字段 ...
/// 可选的系统提示词,覆盖默认配置
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(example = "你是一个专业的 Rust 开发者")]
pub system_prompt: Option<String>,
/// 可选的用户提示词模板,支持 {user_prompt} 变量替换
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schema(example = "请用 Rust 完成:{user_prompt}")]
pub user_prompt: Option<String>,
/// 可选的 Agent 运行时配置(Agent 服务器 + MCP 服务器)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_config: Option<ChatAgentConfig>,
}
验证点:
cargo build -p rcoder编译通过- OpenAPI 文档正确生成新字段
2.1.5 修改 gRPC 客户端传参
文件: crates/rcoder/src/grpc/chat_client.rs
修改内容:
- 在构建 gRPC
ChatRequest时传递新字段:
// 找到构建 GrpcChatRequest 的位置,添加新字段
let grpc_request = GrpcChatRequest {
// ... 现有字段 ...
system_prompt: http_request.system_prompt.clone(),
user_prompt: http_request.user_prompt.clone(),
agent_config: http_request.agent_config.clone().map(|c| c.into()),
};
- 实现
From<ChatAgentConfig>转换(如需要):
impl From<shared_types::ChatAgentConfig> for proto::ChatAgentConfig {
fn from(config: shared_types::ChatAgentConfig) -> Self {
Self {
agent_server: config.agent_server.map(|s| s.into()),
context_servers: config.context_servers
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
}
}
}
验证点:
cargo build -p rcoder编译通过- gRPC 请求正确包含新字段
3. 第二批:核心逻辑
3.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|---|---|---|---|
| 2.1 | 创建 PromptConfigAssembler | crates/agent_config/src/config/prompt_assembler.rs |
新增 |
| 2.2 | 导出新模块 | crates/agent_config/src/config/mod.rs |
修改 |
| 2.3 | 修改 ChatPrompt 结构体 | crates/shared_types/src/lib.rs |
修改 |
| 2.4 | 修改 PromptMessage 结构体 | crates/agent_abstraction/src/traits/agent.rs |
修改 |
3.1.1 创建 PromptConfigAssembler
文件: crates/agent_config/src/config/prompt_assembler.rs
步骤:
- 创建新文件
- 实现
PromptConfigAssembler结构体 - 实现核心方法:
new()- 构造函数with_system_prompt()- Builder 方法with_user_prompt_template()- Builder 方法with_agent_config()- Builder 方法get_system_prompt()- 获取最终系统提示词apply_user_prompt()- 应用用户提示词模板get_agent_server_config()- 获取最终 Agent 配置get_context_servers()- 获取最终 MCP 配置get_agent_id()- 获取使用的 Agent ID
代码模板:
// 见设计文档 4.5.1 节完整代码
验证点:
- 编译通过
- 单元测试:入参优先级正确
- 单元测试:模板替换正确
- 单元测试:配置合并正确
3.1.2 导出新模块
文件: crates/agent_config/src/config/mod.rs
修改内容:
pub mod prompt_assembler;
pub use prompt_assembler::PromptConfigAssembler;
文件: crates/agent_config/src/lib.rs
修改内容:
pub use config::prompt_assembler::PromptConfigAssembler;
验证点:
cargo build -p agent_config编译通过- 其他 crate 可以正常引用
PromptConfigAssembler
3.1.3 修改 ChatPrompt 结构体
文件: crates/shared_types/src/lib.rs (或 chat_prompt.rs)
修改内容:
use crate::ChatAgentConfig;
#[derive(Debug, Clone, Builder)]
pub struct ChatPrompt {
// ... 现有字段 ...
/// 可选的系统提示词覆盖
#[builder(default)]
pub system_prompt_override: Option<String>,
/// 可选的用户提示词模板覆盖
#[builder(default)]
pub user_prompt_template_override: Option<String>,
/// 可选的 Agent 运行时配置覆盖
#[builder(default)]
pub agent_config_override: Option<ChatAgentConfig>,
}
验证点:
cargo build -p shared_types编译通过- Builder 模式正常工作
3.1.4 修改 PromptMessage 结构体
文件: crates/agent_abstraction/src/traits/agent.rs
修改内容:
- 添加 import:
use shared_types::ChatAgentConfig;
- 在
PromptMessage结构体中添加新字段:
#[derive(Debug, Clone)]
pub struct PromptMessage {
// ... 现有字段 ...
/// 系统提示词覆盖
pub system_prompt_override: Option<String>,
/// 用户提示词模板覆盖
pub user_prompt_template_override: Option<String>,
/// Agent 运行时配置覆盖(MCP 服务器等)
pub agent_config_override: Option<ChatAgentConfig>,
}
- 修改
From<ChatPrompt>实现:
impl From<shared_types::ChatPrompt> for PromptMessage {
fn from(chat_prompt: shared_types::ChatPrompt) -> Self {
Self {
// ... 现有字段映射 ...
system_prompt_override: chat_prompt.system_prompt_override,
user_prompt_template_override: chat_prompt.user_prompt_template_override,
agent_config_override: chat_prompt.agent_config_override,
}
}
}
验证点:
cargo build -p agent_abstraction编译通过From转换正确
4. 第三批:服务层集成
4.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|---|---|---|---|
| 3.1 | 修改 gRPC AgentServiceImpl | crates/agent_runner/src/grpc/agent_service_impl.rs |
修改 |
| 3.2 | 修改 AcpAgentWorker | crates/agent_abstraction/src/session/acp_worker.rs |
修改 |
| 3.3 | 修改 ClaudeCodeLauncher | crates/agent_abstraction/src/compat/claude_code_launcher.rs |
修改 |
4.1.1 修改 gRPC AgentServiceImpl
文件: crates/agent_runner/src/grpc/agent_service_impl.rs
修改内容:
在 chat() 方法中:
- 添加 import:
use shared_types::ChatAgentConfig;
- 解析新字段并传递给 ChatPrompt:
async fn chat(&self, request: Request<GrpcChatRequest>) -> Result<Response<GrpcChatResponse>, Status> {
let req = request.into_inner();
// ... 现有验证逻辑 ...
// 转换 gRPC ChatAgentConfig -> shared_types ChatAgentConfig
let agent_config_override: Option<ChatAgentConfig> = req.agent_config.map(|c| c.into());
// 构建 ChatPrompt(包含覆盖配置)
let chat_prompt = ChatPromptBuilder::default()
.project_id(project_id.clone())
.project_path(project_dir)
.session_id(session_id.clone())
.prompt(req.prompt)
.system_prompt_override(req.system_prompt) // 新增
.user_prompt_template_override(req.user_prompt) // 新增
.agent_config_override(agent_config_override) // 新增
// ... 其他字段 ...
.build()
.map_err(|e| Status::internal(format!("构建 ChatPrompt 失败: {}", e)))?;
// ... 后续处理 ...
}
- 实现 gRPC 类型转换(如果需要):
impl From<proto::ChatAgentConfig> for shared_types::ChatAgentConfig {
fn from(proto: proto::ChatAgentConfig) -> Self {
Self {
agent_server: proto.agent_server.map(|s| s.into()),
context_servers: proto.context_servers
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
}
}
}
impl From<proto::ChatAgentServerConfig> for shared_types::ChatAgentServerConfig {
fn from(proto: proto::ChatAgentServerConfig) -> Self {
Self {
agent_id: proto.agent_id,
command: proto.command,
args: if proto.args.is_empty() { None } else { Some(proto.args) },
env: if proto.env.is_empty() { None } else { Some(proto.env) },
metadata: if proto.metadata.is_empty() { None } else { Some(proto.metadata) },
}
}
}
impl From<proto::ChatContextServerConfig> for shared_types::ChatContextServerConfig {
fn from(proto: proto::ChatContextServerConfig) -> Self {
Self {
source: proto.source,
enabled: proto.enabled,
command: proto.command,
args: if proto.args.is_empty() { None } else { Some(proto.args) },
env: if proto.env.is_empty() { None } else { Some(proto.env) },
}
}
}
验证点:
cargo build -p agent_runner编译通过- gRPC 类型转换正确
4.1.2 修改 AcpAgentWorker
文件: crates/agent_abstraction/src/session/acp_worker.rs
修改内容:
- 添加 import:
use agent_config::{AgentServersConfig, PromptConfigAssembler};
- 在处理请求时使用配置组装器:
impl AgentWorker for AcpAgentWorker {
async fn process_request(&self, request: WorkerRequest) -> Result<WorkerResponse> {
// 加载默认配置
let default_config = AgentServersConfig::load_or_default().await;
// 创建配置组装器
let assembler = PromptConfigAssembler::new(default_config)
.with_system_prompt(request.prompt_message.system_prompt_override.clone())
.with_user_prompt_template(request.prompt_message.user_prompt_template_override.clone())
.with_agent_config(request.prompt_message.agent_config_override.clone());
// 获取最终的系统提示词
let system_prompt = assembler.get_system_prompt("claude-code-acp");
// 获取最终的 Agent 配置
let agent_config = assembler.get_agent_server_config("claude-code-acp");
// 获取 MCP 服务器配置
let context_servers = assembler.get_context_servers();
// 应用用户提示词模板
let final_user_prompt = assembler.apply_user_prompt(
"claude-code-acp",
&request.prompt_message.content,
);
// 更新 prompt_message 的 content 为处理后的用户提示词
let mut prompt_message = request.prompt_message.clone();
prompt_message.content = final_user_prompt;
// 构建 AgentStartConfig
let start_config = AgentStartConfig::new()
.with_system_prompt(system_prompt)
.with_mcp_servers(context_servers); // 如果有此方法
// ... 创建会话 / 发送 prompt ...
}
}
验证点:
cargo build -p agent_abstraction编译通过- 配置组装逻辑正确
4.1.3 修改 ClaudeCodeLauncher
文件: crates/agent_abstraction/src/compat/claude_code_launcher.rs
修改内容:
- 添加 import:
use shared_types::ChatAgentConfig;
use agent_config::PromptConfigAssembler;
- 修改或新增配置加载方法:
/// 加载 Agent 配置,支持覆盖
pub async fn load_agent_config_with_override(
model_provider: Option<&ModelProviderConfig>,
agent_config_override: Option<&ChatAgentConfig>,
) -> Result<AgentLaunchConfig> {
// 加载默认配置
let default_config = AgentServersConfig::load_or_default().await;
// 创建配置组装器
let assembler = PromptConfigAssembler::new(default_config.clone())
.with_agent_config(agent_config_override.cloned());
// 获取最终的 Agent 配置
let agent_config = assembler.get_agent_server_config("claude-code-acp");
// 获取 MCP 服务器配置
let context_servers = assembler.get_context_servers();
// 构建启动配置
// ... 使用 agent_config 和 context_servers 构建 AgentLaunchConfig ...
}
- 在
launch()方法中使用新配置:
pub async fn launch(
&self,
project_id: String,
project_path: PathBuf,
session_id_hint: Option<String>,
model_provider: Option<ModelProviderConfig>,
start_config: AgentStartConfig, // 已包含系统提示词和 MCP 配置
client: C,
) -> Result<ConnectionInfo> {
// 从 start_config 获取配置
let system_prompt = start_config.system_prompt.clone();
let mcp_servers = start_config.mcp_servers.clone();
// ... 使用配置启动 Agent ...
}
验证点:
cargo build -p agent_abstraction编译通过- Agent 启动时正确使用覆盖配置
5. 第四批:测试和文档
5.1 任务清单
| 序号 | 任务 | 文件 | 类型 |
|---|---|---|---|
| 4.1 | ChatAgentConfig 单元测试 | crates/shared_types/src/chat_agent_config.rs |
修改 |
| 4.2 | PromptConfigAssembler 单元测试 | crates/agent_config/src/config/prompt_assembler.rs |
修改 |
| 4.3 | 集成测试 | tests/integration/chat_config_test.rs |
新增 |
| 4.4 | 更新 OpenAPI 文档 | 自动生成 | - |
5.1.1 ChatAgentConfig 单元测试
文件: crates/shared_types/src/chat_agent_config.rs
测试用例:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chat_agent_config_default() {
let config = ChatAgentConfig::default();
assert!(config.agent_server.is_none());
assert!(config.context_servers.is_empty());
assert!(!config.has_agent_server());
assert!(!config.has_context_servers());
}
#[test]
fn test_chat_agent_config_json_serialize() {
let config = ChatAgentConfig {
agent_server: Some(ChatAgentServerConfig {
agent_id: Some("test-agent".to_string()),
command: Some("test-cmd".to_string()),
..Default::default()
}),
context_servers: HashMap::new(),
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("test-agent"));
}
#[test]
fn test_chat_agent_config_json_deserialize() {
let json = r#"{
"agent_server": {
"agent_id": "claude-code-acp",
"env": {"RUST_LOG": "debug"}
},
"context_servers": {
"context7": {
"source": "custom",
"enabled": true,
"command": "bunx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
}"#;
let config: ChatAgentConfig = serde_json::from_str(json).unwrap();
assert!(config.has_agent_server());
assert!(config.has_context_servers());
assert_eq!(config.agent_server.unwrap().get_agent_id(), "claude-code-acp");
}
#[test]
fn test_get_agent_id_default() {
let config = ChatAgentServerConfig::default();
assert_eq!(config.get_agent_id(), "claude-code-acp");
}
#[test]
fn test_get_enabled_context_servers() {
let mut context_servers = HashMap::new();
context_servers.insert("enabled".to_string(), ChatContextServerConfig {
enabled: true,
..Default::default()
});
context_servers.insert("disabled".to_string(), ChatContextServerConfig {
enabled: false,
..Default::default()
});
let config = ChatAgentConfig {
agent_server: None,
context_servers,
};
let enabled = config.get_enabled_context_servers();
assert_eq!(enabled.len(), 1);
assert!(enabled.contains_key("enabled"));
}
}
5.1.2 PromptConfigAssembler 单元测试
文件: crates/agent_config/src/config/prompt_assembler.rs
测试用例:
#[cfg(test)]
mod tests {
use super::*;
fn create_default_config() -> AgentServersConfig {
// 创建测试用的默认配置
AgentServersConfig::default()
}
#[test]
fn test_system_prompt_override() {
let assembler = PromptConfigAssembler::new(create_default_config())
.with_system_prompt(Some("自定义系统提示词".to_string()));
let result = assembler.get_system_prompt("claude-code-acp");
assert_eq!(result, "自定义系统提示词");
}
#[test]
fn test_system_prompt_use_default() {
let assembler = PromptConfigAssembler::new(create_default_config());
// 应该使用默认配置
let result = assembler.get_system_prompt("claude-code-acp");
// 验证使用了默认配置
}
#[test]
fn test_user_prompt_template() {
let assembler = PromptConfigAssembler::new(create_default_config())
.with_user_prompt_template(Some("请用中文回答:{user_prompt}".to_string()));
let result = assembler.apply_user_prompt("claude-code-acp", "hello world");
assert_eq!(result, "请用中文回答:hello world");
}
#[test]
fn test_user_prompt_no_template() {
let assembler = PromptConfigAssembler::new(create_default_config());
let result = assembler.apply_user_prompt("claude-code-acp", "hello world");
assert_eq!(result, "hello world");
}
#[test]
fn test_agent_server_config_merge() {
let override_config = ChatAgentConfig {
agent_server: Some(ChatAgentServerConfig {
env: Some(HashMap::from([
("RUST_LOG".to_string(), "debug".to_string()),
])),
..Default::default()
}),
context_servers: HashMap::new(),
};
let assembler = PromptConfigAssembler::new(create_default_config())
.with_agent_config(Some(override_config));
let result = assembler.get_agent_server_config("claude-code-acp");
// 验证 env 被正确合并
assert!(result.env.contains_key("RUST_LOG"));
}
#[test]
fn test_context_servers_override() {
let mut context_servers = HashMap::new();
context_servers.insert("my-mcp".to_string(), ChatContextServerConfig {
source: "custom".to_string(),
enabled: true,
command: Some("bunx".to_string()),
args: Some(vec!["-y".to_string(), "my-mcp-server".to_string()]),
env: None,
});
let override_config = ChatAgentConfig {
agent_server: None,
context_servers,
};
let assembler = PromptConfigAssembler::new(create_default_config())
.with_agent_config(Some(override_config));
let result = assembler.get_context_servers();
assert!(result.contains_key("my-mcp"));
}
}
5.1.3 集成测试
文件: tests/integration/chat_config_test.rs (或在现有测试文件中添加)
测试用例:
#[tokio::test]
async fn test_chat_with_system_prompt() {
// 测试只传 system_prompt
let response = client
.post("/chat")
.json(&json!({
"prompt": "hello",
"project_id": "test",
"system_prompt": "你是 Rust 专家"
}))
.send()
.await
.unwrap();
assert!(response.status().is_success());
}
#[tokio::test]
async fn test_chat_with_user_prompt_template() {
// 测试 user_prompt 模板替换
let response = client
.post("/chat")
.json(&json!({
"prompt": "hello world",
"project_id": "test",
"user_prompt": "请用中文回答:{user_prompt}"
}))
.send()
.await
.unwrap();
assert!(response.status().is_success());
}
#[tokio::test]
async fn test_chat_with_agent_config() {
// 测试完整的 agent_config
let response = client
.post("/chat")
.json(&json!({
"prompt": "hello",
"project_id": "test",
"agent_config": {
"agent_server": {
"env": {"RUST_LOG": "debug"}
},
"context_servers": {
"context7": {
"enabled": true,
"command": "bunx",
"args": ["-y", "@upstash/context7-mcp"]
}
}
}
}))
.send()
.await
.unwrap();
assert!(response.status().is_success());
}
6. 实现检查清单
6.1 第一批完成检查
ChatAgentConfig结构体创建完成ChatAgentServerConfig结构体创建完成ChatContextServerConfig结构体创建完成shared_types导出新类型agent.proto添加新消息定义- HTTP
ChatRequest添加新字段 - gRPC 客户端传递新字段
cargo build全部通过
6.2 第二批完成检查
PromptConfigAssembler创建完成agent_config导出新模块ChatPrompt添加新字段PromptMessage添加新字段From转换实现完成cargo build全部通过
6.3 第三批完成检查
AgentServiceImpl::chat()处理新字段- gRPC 类型转换实现完成
AcpAgentWorker使用配置组装器ClaudeCodeLauncher支持配置覆盖cargo build全部通过
6.4 第四批完成检查
ChatAgentConfig单元测试通过PromptConfigAssembler单元测试通过- 集成测试通过
- OpenAPI 文档更新
cargo test全部通过
7. 风险和注意事项
7.1 向后兼容性
- 所有新字段都是
Option类型,确保旧客户端正常工作 - proto 字段编号从 8 开始,不影响现有字段
7.2 类型转换
- 注意 gRPC 生成的类型和 Rust 原生类型的转换
repeated字段为空时转换为Nonemap字段为空时转换为None
7.3 配置合并逻辑
env和metadata是合并(入参优先)args是替换(入参完全覆盖)- 提示词是覆盖(入参有值则使用入参)
7.4 测试覆盖
- 确保测试覆盖所有配置组合
- 特别注意边界情况:空字符串、空 HashMap 等
8. 预计工作量
| 批次 | 预计工作量 | 依赖 |
|---|---|---|
| 第一批 | 2-3 小时 | 无 |
| 第二批 | 2-3 小时 | 第一批 |
| 第三批 | 2-3 小时 | 第二批 |
| 第四批 | 1-2 小时 | 第三批 |
总计: 约 7-11 小时