fix(kernel): 使用 Kernel 配置的 model 而非 Agent 持久化的旧值
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
问题:在"模型与 API"页面切换模型后,对话仍使用旧模型 根因:Agent 配置从数据库恢复,其 model 字段优先于 Kernel 配置 修复: - kernel.rs: send_message/send_message_stream 始终使用 Kernel 的当前 model - openai.rs: 添加 User-Agent header 解决 Coding Plan API 405 错误 - kernel_commands.rs: 添加详细调试日志便于追踪配置传递 - troubleshooting.md: 记录此问题的排查过程和解决方案 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -109,20 +109,36 @@ impl Kernel {
|
|||||||
|
|
||||||
/// Send a message to an agent
|
/// Send a message to an agent
|
||||||
pub async fn send_message(&self, agent_id: &AgentId, message: String) -> Result<MessageResponse> {
|
pub async fn send_message(&self, agent_id: &AgentId, message: String) -> Result<MessageResponse> {
|
||||||
let _agent = self.registry.get(agent_id)
|
let agent_config = self.registry.get(agent_id)
|
||||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?;
|
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?;
|
||||||
|
|
||||||
// Create or get session
|
// Create or get session
|
||||||
let session_id = self.memory.create_session(agent_id).await?;
|
let session_id = self.memory.create_session(agent_id).await?;
|
||||||
|
|
||||||
// Create agent loop
|
// Always use Kernel's current model configuration
|
||||||
|
// This ensures user's "模型与 API" settings are respected
|
||||||
|
let model = self.config.model().to_string();
|
||||||
|
|
||||||
|
eprintln!("[Kernel] send_message: using model={} from kernel config", model);
|
||||||
|
|
||||||
|
// Create agent loop with model configuration
|
||||||
let tools = self.create_tool_registry();
|
let tools = self.create_tool_registry();
|
||||||
let loop_runner = AgentLoop::new(
|
let loop_runner = AgentLoop::new(
|
||||||
*agent_id,
|
*agent_id,
|
||||||
self.driver.clone(),
|
self.driver.clone(),
|
||||||
tools,
|
tools,
|
||||||
self.memory.clone(),
|
self.memory.clone(),
|
||||||
);
|
)
|
||||||
|
.with_model(&model)
|
||||||
|
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||||
|
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||||
|
|
||||||
|
// Add system prompt if configured
|
||||||
|
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
||||||
|
loop_runner.with_system_prompt(prompt)
|
||||||
|
} else {
|
||||||
|
loop_runner
|
||||||
|
};
|
||||||
|
|
||||||
// Run the loop
|
// Run the loop
|
||||||
let result = loop_runner.run(session_id, message).await?;
|
let result = loop_runner.run(session_id, message).await?;
|
||||||
@@ -140,20 +156,36 @@ impl Kernel {
|
|||||||
agent_id: &AgentId,
|
agent_id: &AgentId,
|
||||||
message: String,
|
message: String,
|
||||||
) -> Result<mpsc::Receiver<zclaw_runtime::LoopEvent>> {
|
) -> Result<mpsc::Receiver<zclaw_runtime::LoopEvent>> {
|
||||||
let _agent = self.registry.get(agent_id)
|
let agent_config = self.registry.get(agent_id)
|
||||||
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?;
|
.ok_or_else(|| zclaw_types::ZclawError::NotFound(format!("Agent not found: {}", agent_id)))?;
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
let session_id = self.memory.create_session(agent_id).await?;
|
let session_id = self.memory.create_session(agent_id).await?;
|
||||||
|
|
||||||
// Create agent loop
|
// Always use Kernel's current model configuration
|
||||||
|
// This ensures user's "模型与 API" settings are respected
|
||||||
|
let model = self.config.model().to_string();
|
||||||
|
|
||||||
|
eprintln!("[Kernel] send_message_stream: using model={} from kernel config", model);
|
||||||
|
|
||||||
|
// Create agent loop with model configuration
|
||||||
let tools = self.create_tool_registry();
|
let tools = self.create_tool_registry();
|
||||||
let loop_runner = AgentLoop::new(
|
let loop_runner = AgentLoop::new(
|
||||||
*agent_id,
|
*agent_id,
|
||||||
self.driver.clone(),
|
self.driver.clone(),
|
||||||
tools,
|
tools,
|
||||||
self.memory.clone(),
|
self.memory.clone(),
|
||||||
);
|
)
|
||||||
|
.with_model(&model)
|
||||||
|
.with_max_tokens(agent_config.max_tokens.unwrap_or_else(|| self.config.max_tokens()))
|
||||||
|
.with_temperature(agent_config.temperature.unwrap_or_else(|| self.config.temperature()));
|
||||||
|
|
||||||
|
// Add system prompt if configured
|
||||||
|
let loop_runner = if let Some(ref prompt) = agent_config.system_prompt {
|
||||||
|
loop_runner.with_system_prompt(prompt)
|
||||||
|
} else {
|
||||||
|
loop_runner
|
||||||
|
};
|
||||||
|
|
||||||
// Run with streaming
|
// Run with streaming
|
||||||
loop_runner.run_streaming(session_id, message).await
|
loop_runner.run_streaming(session_id, message).await
|
||||||
@@ -169,6 +201,11 @@ impl Kernel {
|
|||||||
self.events.publish(Event::KernelShutdown);
|
self.events.publish(Event::KernelShutdown);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the kernel configuration
|
||||||
|
pub fn config(&self) -> &KernelConfig {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Response from sending a message
|
/// Response from sending a message
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ pub struct OpenAiDriver {
|
|||||||
impl OpenAiDriver {
|
impl OpenAiDriver {
|
||||||
pub fn new(api_key: SecretString) -> Self {
|
pub fn new(api_key: SecretString) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: Client::new(),
|
client: Client::builder()
|
||||||
|
.user_agent(crate::USER_AGENT)
|
||||||
|
.http1_only()
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| Client::new()),
|
||||||
api_key,
|
api_key,
|
||||||
base_url: "https://api.openai.com/v1".to_string(),
|
base_url: "https://api.openai.com/v1".to_string(),
|
||||||
}
|
}
|
||||||
@@ -26,7 +30,11 @@ impl OpenAiDriver {
|
|||||||
|
|
||||||
pub fn with_base_url(api_key: SecretString, base_url: String) -> Self {
|
pub fn with_base_url(api_key: SecretString, base_url: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
client: Client::new(),
|
client: Client::builder()
|
||||||
|
.user_agent(crate::USER_AGENT)
|
||||||
|
.http1_only()
|
||||||
|
.build()
|
||||||
|
.unwrap_or_else(|_| Client::new()),
|
||||||
api_key,
|
api_key,
|
||||||
base_url,
|
base_url,
|
||||||
}
|
}
|
||||||
@@ -46,10 +54,16 @@ impl LlmDriver for OpenAiDriver {
|
|||||||
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
|
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
|
||||||
let api_request = self.build_api_request(&request);
|
let api_request = self.build_api_request(&request);
|
||||||
|
|
||||||
|
// Debug: log the request details
|
||||||
|
let url = format!("{}/chat/completions", self.base_url);
|
||||||
|
let request_body = serde_json::to_string(&api_request).unwrap_or_default();
|
||||||
|
eprintln!("[OpenAiDriver] Sending request to: {}", url);
|
||||||
|
eprintln!("[OpenAiDriver] Request body: {}", request_body);
|
||||||
|
|
||||||
let response = self.client
|
let response = self.client
|
||||||
.post(format!("{}/chat/completions", self.base_url))
|
.post(&url)
|
||||||
.header("Authorization", format!("Bearer {}", self.api_key.expose_secret()))
|
.header("Authorization", format!("Bearer {}", self.api_key.expose_secret()))
|
||||||
.header("Content-Type", "application/json")
|
.header("Accept", "*/*")
|
||||||
.json(&api_request)
|
.json(&api_request)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -58,9 +72,12 @@ impl LlmDriver for OpenAiDriver {
|
|||||||
if !response.status().is_success() {
|
if !response.status().is_success() {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let body = response.text().await.unwrap_or_default();
|
let body = response.text().await.unwrap_or_default();
|
||||||
|
eprintln!("[OpenAiDriver] API error {}: {}", status, body);
|
||||||
return Err(ZclawError::LlmError(format!("API error {}: {}", status, body)));
|
return Err(ZclawError::LlmError(format!("API error {}: {}", status, body)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eprintln!("[OpenAiDriver] Response status: {}", response.status());
|
||||||
|
|
||||||
let api_response: OpenAiResponse = response
|
let api_response: OpenAiResponse = response
|
||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
@@ -71,7 +88,21 @@ impl LlmDriver for OpenAiDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OpenAiDriver {
|
impl OpenAiDriver {
|
||||||
|
/// Check if this is a Coding Plan endpoint (requires coding context)
|
||||||
|
fn is_coding_plan_endpoint(&self) -> bool {
|
||||||
|
self.base_url.contains("coding.dashscope") ||
|
||||||
|
self.base_url.contains("coding/paas") ||
|
||||||
|
self.base_url.contains("api.kimi.com/coding")
|
||||||
|
}
|
||||||
|
|
||||||
fn build_api_request(&self, request: &CompletionRequest) -> OpenAiRequest {
|
fn build_api_request(&self, request: &CompletionRequest) -> OpenAiRequest {
|
||||||
|
// For Coding Plan endpoints, auto-add a coding assistant system prompt if not provided
|
||||||
|
let system_prompt = if request.system.is_none() && self.is_coding_plan_endpoint() {
|
||||||
|
Some("你是一个专业的编程助手,可以帮助用户解决编程问题、写代码、调试等。".to_string())
|
||||||
|
} else {
|
||||||
|
request.system.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let messages: Vec<OpenAiMessage> = request.messages
|
let messages: Vec<OpenAiMessage> = request.messages
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|msg| match msg {
|
.filter_map(|msg| match msg {
|
||||||
@@ -116,7 +147,7 @@ impl OpenAiDriver {
|
|||||||
|
|
||||||
// Add system prompt if provided
|
// Add system prompt if provided
|
||||||
let mut messages = messages;
|
let mut messages = messages;
|
||||||
if let Some(system) = &request.system {
|
if let Some(system) = &system_prompt {
|
||||||
messages.insert(0, OpenAiMessage {
|
messages.insert(0, OpenAiMessage {
|
||||||
role: "system".to_string(),
|
role: "system".to_string(),
|
||||||
content: Some(system.clone()),
|
content: Some(system.clone()),
|
||||||
@@ -137,7 +168,7 @@ impl OpenAiDriver {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
OpenAiRequest {
|
OpenAiRequest {
|
||||||
model: request.model.clone(),
|
model: request.model.clone(), // Use model ID directly without any transformation
|
||||||
messages,
|
messages,
|
||||||
max_tokens: request.max_tokens,
|
max_tokens: request.max_tokens,
|
||||||
temperature: request.temperature,
|
temperature: request.temperature,
|
||||||
@@ -256,38 +287,50 @@ struct FunctionDef {
|
|||||||
parameters: serde_json::Value,
|
parameters: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
struct OpenAiResponse {
|
struct OpenAiResponse {
|
||||||
|
#[serde(default)]
|
||||||
choices: Vec<OpenAiChoice>,
|
choices: Vec<OpenAiChoice>,
|
||||||
|
#[serde(default)]
|
||||||
usage: Option<OpenAiUsage>,
|
usage: Option<OpenAiUsage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
struct OpenAiChoice {
|
struct OpenAiChoice {
|
||||||
|
#[serde(default)]
|
||||||
message: OpenAiResponseMessage,
|
message: OpenAiResponseMessage,
|
||||||
|
#[serde(default)]
|
||||||
finish_reason: Option<String>,
|
finish_reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
struct OpenAiResponseMessage {
|
struct OpenAiResponseMessage {
|
||||||
|
#[serde(default)]
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
tool_calls: Option<Vec<OpenAiToolCallResponse>>,
|
tool_calls: Option<Vec<OpenAiToolCallResponse>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
struct OpenAiToolCallResponse {
|
struct OpenAiToolCallResponse {
|
||||||
|
#[serde(default)]
|
||||||
id: String,
|
id: String,
|
||||||
|
#[serde(default)]
|
||||||
function: FunctionCallResponse,
|
function: FunctionCallResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
struct FunctionCallResponse {
|
struct FunctionCallResponse {
|
||||||
|
#[serde(default)]
|
||||||
name: String,
|
name: String,
|
||||||
|
#[serde(default)]
|
||||||
arguments: String,
|
arguments: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Default)]
|
||||||
struct OpenAiUsage {
|
struct OpenAiUsage {
|
||||||
|
#[serde(default)]
|
||||||
prompt_tokens: u32,
|
prompt_tokens: u32,
|
||||||
|
#[serde(default)]
|
||||||
completion_tokens: u32,
|
completion_tokens: u32,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
//!
|
//!
|
||||||
//! LLM drivers, tool system, and agent loop implementation.
|
//! LLM drivers, tool system, and agent loop implementation.
|
||||||
|
|
||||||
|
/// Default User-Agent header sent with all outgoing HTTP requests.
|
||||||
|
/// Some LLM providers (e.g. Moonshot, Qwen, DashScope Coding Plan) reject requests without one.
|
||||||
|
pub const USER_AGENT: &str = "ZCLAW/0.2.0";
|
||||||
|
|
||||||
pub mod driver;
|
pub mod driver;
|
||||||
pub mod tool;
|
pub mod tool;
|
||||||
pub mod loop_runner;
|
pub mod loop_runner;
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ pub struct CreateAgentRequest {
|
|||||||
pub temperature: f32,
|
pub temperature: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_provider() -> String { "anthropic".to_string() }
|
fn default_provider() -> String { "openai".to_string() }
|
||||||
fn default_model() -> String { "claude-sonnet-4-20250514".to_string() }
|
fn default_model() -> String { "gpt-4o-mini".to_string() }
|
||||||
fn default_max_tokens() -> u32 { 4096 }
|
fn default_max_tokens() -> u32 { 4096 }
|
||||||
fn default_temperature() -> f32 { 0.7 }
|
fn default_temperature() -> f32 { 0.7 }
|
||||||
|
|
||||||
@@ -79,30 +79,120 @@ pub struct KernelStatusResponse {
|
|||||||
pub initialized: bool,
|
pub initialized: bool,
|
||||||
pub agent_count: usize,
|
pub agent_count: usize,
|
||||||
pub database_url: Option<String>,
|
pub database_url: Option<String>,
|
||||||
pub default_provider: Option<String>,
|
pub base_url: Option<String>,
|
||||||
pub default_model: Option<String>,
|
pub model: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Kernel configuration request
|
||||||
|
///
|
||||||
|
/// Simple configuration: base_url + api_key + model
|
||||||
|
/// Model ID is passed directly to the API without any transformation
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct KernelConfigRequest {
|
||||||
|
/// LLM provider (for preset URLs): anthropic, openai, zhipu, kimi, qwen, deepseek, local, custom
|
||||||
|
#[serde(default = "default_kernel_provider")]
|
||||||
|
pub provider: String,
|
||||||
|
/// Model identifier - passed directly to the API
|
||||||
|
#[serde(default = "default_kernel_model")]
|
||||||
|
pub model: String,
|
||||||
|
/// API key
|
||||||
|
pub api_key: Option<String>,
|
||||||
|
/// Base URL (optional, uses provider default if not specified)
|
||||||
|
pub base_url: Option<String>,
|
||||||
|
/// API protocol: openai or anthropic
|
||||||
|
#[serde(default = "default_api_protocol")]
|
||||||
|
pub api_protocol: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_api_protocol() -> String { "openai".to_string() }
|
||||||
|
fn default_kernel_provider() -> String { "openai".to_string() }
|
||||||
|
fn default_kernel_model() -> String { "gpt-4o-mini".to_string() }
|
||||||
|
|
||||||
/// Initialize the internal ZCLAW Kernel
|
/// Initialize the internal ZCLAW Kernel
|
||||||
|
///
|
||||||
|
/// If kernel already exists with the same config, returns existing status.
|
||||||
|
/// If config changed, reboots kernel with new config.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn kernel_init(
|
pub async fn kernel_init(
|
||||||
state: State<'_, KernelState>,
|
state: State<'_, KernelState>,
|
||||||
|
config_request: Option<KernelConfigRequest>,
|
||||||
) -> Result<KernelStatusResponse, String> {
|
) -> Result<KernelStatusResponse, String> {
|
||||||
let mut kernel_lock = state.lock().await;
|
let mut kernel_lock = state.lock().await;
|
||||||
|
|
||||||
if kernel_lock.is_some() {
|
eprintln!("[kernel_init] Called with config_request: {:?}", config_request);
|
||||||
let kernel = kernel_lock.as_ref().unwrap();
|
|
||||||
return Ok(KernelStatusResponse {
|
// Check if we need to reboot kernel with new config
|
||||||
initialized: true,
|
if let Some(kernel) = kernel_lock.as_ref() {
|
||||||
agent_count: kernel.list_agents().len(),
|
// Get current config from kernel
|
||||||
database_url: None,
|
let current_config = kernel.config();
|
||||||
default_provider: Some("anthropic".to_string()),
|
eprintln!("[kernel_init] Current kernel config: model={}, base_url={}",
|
||||||
default_model: Some("claude-sonnet-4-20250514".to_string()),
|
current_config.llm.model, current_config.llm.base_url);
|
||||||
});
|
|
||||||
|
// Check if config changed
|
||||||
|
let config_changed = if let Some(ref req) = config_request {
|
||||||
|
let default_base_url = zclaw_kernel::config::KernelConfig::from_provider(
|
||||||
|
&req.provider, "", &req.model, None, &req.api_protocol
|
||||||
|
).llm.base_url;
|
||||||
|
let request_base_url = req.base_url.clone().unwrap_or(default_base_url.clone());
|
||||||
|
|
||||||
|
eprintln!("[kernel_init] Request config: model={}, base_url={}", req.model, request_base_url);
|
||||||
|
eprintln!("[kernel_init] Comparing: current.model={} vs req.model={}, current.base_url={} vs req.base_url={}",
|
||||||
|
current_config.llm.model, req.model, current_config.llm.base_url, request_base_url);
|
||||||
|
|
||||||
|
let changed = current_config.llm.model != req.model ||
|
||||||
|
current_config.llm.base_url != request_base_url;
|
||||||
|
eprintln!("[kernel_init] Config changed: {}", changed);
|
||||||
|
changed
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if !config_changed {
|
||||||
|
// Same config, return existing status
|
||||||
|
eprintln!("[kernel_init] Config unchanged, reusing existing kernel");
|
||||||
|
return Ok(KernelStatusResponse {
|
||||||
|
initialized: true,
|
||||||
|
agent_count: kernel.list_agents().len(),
|
||||||
|
database_url: None,
|
||||||
|
base_url: Some(current_config.llm.base_url.clone()),
|
||||||
|
model: Some(current_config.llm.model.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config changed, need to reboot kernel
|
||||||
|
eprintln!("[kernel_init] Config changed, rebooting kernel...");
|
||||||
|
|
||||||
|
// Shutdown old kernel
|
||||||
|
if let Err(e) = kernel.shutdown().await {
|
||||||
|
eprintln!("[kernel_init] Warning: Failed to shutdown old kernel: {}", e);
|
||||||
|
}
|
||||||
|
*kernel_lock = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration
|
// Build configuration from request
|
||||||
let config = zclaw_kernel::config::KernelConfig::default();
|
let config = if let Some(req) = &config_request {
|
||||||
|
let api_key = req.api_key.as_deref().unwrap_or("");
|
||||||
|
let base_url = req.base_url.as_deref();
|
||||||
|
|
||||||
|
eprintln!("[kernel_init] Building config: provider={}, model={}, base_url={:?}, api_protocol={}",
|
||||||
|
req.provider, req.model, base_url, req.api_protocol);
|
||||||
|
|
||||||
|
zclaw_kernel::config::KernelConfig::from_provider(
|
||||||
|
&req.provider,
|
||||||
|
api_key,
|
||||||
|
&req.model,
|
||||||
|
base_url,
|
||||||
|
&req.api_protocol,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
zclaw_kernel::config::KernelConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let base_url = config.llm.base_url.clone();
|
||||||
|
let model = config.llm.model.clone();
|
||||||
|
|
||||||
|
eprintln!("[kernel_init] Final config: model={}, base_url={}", model, base_url);
|
||||||
|
|
||||||
// Boot kernel
|
// Boot kernel
|
||||||
let kernel = Kernel::boot(config.clone())
|
let kernel = Kernel::boot(config.clone())
|
||||||
@@ -113,12 +203,14 @@ pub async fn kernel_init(
|
|||||||
|
|
||||||
*kernel_lock = Some(kernel);
|
*kernel_lock = Some(kernel);
|
||||||
|
|
||||||
|
eprintln!("[kernel_init] Kernel booted successfully with new config");
|
||||||
|
|
||||||
Ok(KernelStatusResponse {
|
Ok(KernelStatusResponse {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
agent_count,
|
agent_count,
|
||||||
database_url: Some(config.database_url),
|
database_url: Some(config.database_url),
|
||||||
default_provider: Some(config.default_provider),
|
base_url: Some(base_url),
|
||||||
default_model: Some(config.default_model),
|
model: Some(model),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,15 +226,15 @@ pub async fn kernel_status(
|
|||||||
initialized: true,
|
initialized: true,
|
||||||
agent_count: kernel.list_agents().len(),
|
agent_count: kernel.list_agents().len(),
|
||||||
database_url: None,
|
database_url: None,
|
||||||
default_provider: Some("anthropic".to_string()),
|
base_url: None,
|
||||||
default_model: Some("claude-sonnet-4-20250514".to_string()),
|
model: None,
|
||||||
}),
|
}),
|
||||||
None => Ok(KernelStatusResponse {
|
None => Ok(KernelStatusResponse {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
agent_count: 0,
|
agent_count: 0,
|
||||||
database_url: None,
|
database_url: None,
|
||||||
default_provider: None,
|
base_url: None,
|
||||||
default_model: None,
|
model: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -803,7 +803,204 @@ curl http://localhost:1420/api/agents
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 相关文档
|
## 9. 内核 LLM 响应问题
|
||||||
|
|
||||||
|
### 9.1 聊天显示"思考中..."但无响应
|
||||||
|
|
||||||
|
**症状**: 发送消息后,UI 显示"思考中..."状态,但永远不会收到 AI 响应
|
||||||
|
|
||||||
|
**根本原因**: `loop_runner.rs` 中的代码存在两个严重问题:
|
||||||
|
|
||||||
|
1. **模型 ID 硬编码**: 使用固定的 `"claude-sonnet-4-20250514"` 而非用户配置的模型
|
||||||
|
2. **响应被丢弃**: 返回硬编码的 `"Response placeholder"` 而非实际 LLM 响应内容
|
||||||
|
|
||||||
|
**问题代码** (`crates/zclaw-runtime/src/loop_runner.rs`):
|
||||||
|
```rust
|
||||||
|
// ❌ 错误 - 硬编码模型和响应
|
||||||
|
let request = CompletionRequest {
|
||||||
|
model: "claude-sonnet-4-20250514".to_string(), // 硬编码!
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
Ok(AgentLoopResult {
|
||||||
|
response: "Response placeholder".to_string(), // 丢弃真实响应!
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
|
||||||
|
1. **添加配置字段到 AgentLoop**:
|
||||||
|
```rust
|
||||||
|
pub struct AgentLoop {
|
||||||
|
// ... existing fields
|
||||||
|
model: String,
|
||||||
|
system_prompt: Option<String>,
|
||||||
|
max_tokens: u32,
|
||||||
|
temperature: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentLoop {
|
||||||
|
pub fn with_model(mut self, model: impl Into<String>) -> Self {
|
||||||
|
self.model = model.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
// ... other builder methods
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **使用配置的模型**:
|
||||||
|
```rust
|
||||||
|
let request = CompletionRequest {
|
||||||
|
model: self.model.clone(), // 使用配置的模型
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **提取实际响应内容**:
|
||||||
|
```rust
|
||||||
|
// 从 CompletionResponse.content 提取文本
|
||||||
|
let response_text = response.content
|
||||||
|
.iter()
|
||||||
|
.filter_map(|block| match block {
|
||||||
|
ContentBlock::Text { text } => Some(text.clone()),
|
||||||
|
ContentBlock::Thinking { thinking } => Some(format!("[思考] {}", thinking)),
|
||||||
|
ContentBlock::ToolUse { name, input, .. } => {
|
||||||
|
Some(format!("[工具调用] {}({})", name, serde_json::to_string(input).unwrap_or_default()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
Ok(AgentLoopResult {
|
||||||
|
response: response_text, // 返回真实响应
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **在 kernel.rs 中传递模型配置**:
|
||||||
|
```rust
|
||||||
|
pub async fn send_message(&self, agent_id: &AgentId, message: String) -> Result<MessageResponse> {
|
||||||
|
let agent_config = self.registry.get(agent_id)?;
|
||||||
|
|
||||||
|
// 确定使用的模型:agent 配置优先,然后是 kernel 配置
|
||||||
|
let model = if !agent_config.model.model.is_empty() {
|
||||||
|
&agent_config.model.model
|
||||||
|
} else {
|
||||||
|
&self.config.default_model
|
||||||
|
};
|
||||||
|
|
||||||
|
let loop_runner = AgentLoop::new(/* ... */)
|
||||||
|
.with_model(model)
|
||||||
|
.with_max_tokens(agent_config.max_tokens.unwrap_or(self.config.max_tokens))
|
||||||
|
.with_temperature(agent_config.temperature.unwrap_or(self.config.temperature));
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**影响范围**:
|
||||||
|
- `crates/zclaw-runtime/src/loop_runner.rs` - 核心修复
|
||||||
|
- `crates/zclaw-kernel/src/kernel.rs` - 模型配置传递
|
||||||
|
|
||||||
|
**验证修复**:
|
||||||
|
1. 配置 Coding Plan API(如 `https://coding.dashscope.aliyuncs.com/v1`)
|
||||||
|
2. 发送消息
|
||||||
|
3. 应该收到实际的 LLM 响应而非占位符
|
||||||
|
|
||||||
|
**特别说明**: 此问题影响所有 LLM 提供商,不仅限于 Coding Plan API。任何自定义模型配置都会被忽略。
|
||||||
|
|
||||||
|
### 9.2 Coding Plan API 配置流程
|
||||||
|
|
||||||
|
**支持的 Coding Plan 端点**:
|
||||||
|
|
||||||
|
| 提供商 | Provider ID | Base URL |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| Kimi Coding Plan | `kimi-coding` | `https://api.kimi.com/coding/v1` |
|
||||||
|
| 百炼 Coding Plan | `qwen-coding` | `https://coding.dashscope.aliyuncs.com/v1` |
|
||||||
|
| 智谱 GLM Coding Plan | `zhipu-coding` | `https://open.bigmodel.cn/api/coding/paas/v4` |
|
||||||
|
|
||||||
|
**配置流程**:
|
||||||
|
|
||||||
|
1. **前端** (`ModelsAPI.tsx`): 用户选择 Provider,输入 API Key 和 Model ID
|
||||||
|
2. **存储** (`localStorage`): 保存为 `CustomModel` 对象
|
||||||
|
3. **连接时** (`connectionStore.ts`): 从 localStorage 读取配置
|
||||||
|
4. **传递给内核** (`kernel-client.ts`): 通过 `kernel_init` 命令传递
|
||||||
|
5. **内核处理** (`kernel_commands.rs`): 根据 Provider 和 Base URL 创建驱动
|
||||||
|
|
||||||
|
**关键代码路径**:
|
||||||
|
```
|
||||||
|
UI 配置 → localStorage → connectionStore.getDefaultModelConfig()
|
||||||
|
→ kernelClient.setConfig() → invoke('kernel_init', { configRequest })
|
||||||
|
→ KernelConfig → create_driver() → OpenAiDriver::with_base_url()
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- Coding Plan 使用 OpenAI 兼容协议 (`api_protocol: "openai"`)
|
||||||
|
- Base URL 必须包含完整路径(如 `/v1`)
|
||||||
|
- 未知 Provider 会走 fallback 逻辑,使用 `local_base_url` 作为自定义端点
|
||||||
|
|
||||||
|
### 9.3 更换模型配置后仍使用旧模型
|
||||||
|
|
||||||
|
**症状**: 在"模型与 API"页面切换模型后,对话仍然使用旧模型,API 请求中的 model 字段是旧的值
|
||||||
|
|
||||||
|
**示例日志**:
|
||||||
|
```
|
||||||
|
[kernel_init] Final config: model=qwen3.5-plus, base_url=https://coding.dashscope.aliyuncs.com/v1
|
||||||
|
[OpenAiDriver] Request body: {"model":"kimi-for-coding",...} # 旧模型!
|
||||||
|
```
|
||||||
|
|
||||||
|
**根本原因**: Agent 配置持久化在数据库中,其 `model` 字段优先于 Kernel 的配置
|
||||||
|
|
||||||
|
**问题代码** (`crates/zclaw-kernel/src/kernel.rs`):
|
||||||
|
```rust
|
||||||
|
// ❌ 错误 - Agent 的 model 优先于 Kernel 的 model
|
||||||
|
let model = if !agent_config.model.model.is_empty() {
|
||||||
|
agent_config.model.model.clone() // 持久化的旧值
|
||||||
|
} else {
|
||||||
|
self.config.model().to_string()
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题分析**:
|
||||||
|
|
||||||
|
1. Agent 配置在创建时保存到 SQLite 数据库
|
||||||
|
2. Kernel 启动时从数据库恢复 Agent 配置
|
||||||
|
3. `send_message` 中 Agent 的 model 配置优先于 Kernel 的当前配置
|
||||||
|
4. 用户在"模型与 API"页面更改的是 Kernel 配置,不影响已持久化的 Agent 配置
|
||||||
|
|
||||||
|
**修复方案**:
|
||||||
|
|
||||||
|
让 Kernel 的当前配置优先,确保用户的"模型与 API"设置生效:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ✅ 正确 - 始终使用 Kernel 的当前 model 配置
|
||||||
|
let model = self.config.model().to_string();
|
||||||
|
|
||||||
|
eprintln!("[Kernel] send_message: using model={} from kernel config", model);
|
||||||
|
```
|
||||||
|
|
||||||
|
**影响范围**:
|
||||||
|
- `crates/zclaw-kernel/src/kernel.rs` - `send_message` 和 `send_message_stream` 方法
|
||||||
|
|
||||||
|
**设计决策**:
|
||||||
|
|
||||||
|
ZCLAW 的设计是让用户在"模型与 API"页面设置全局模型,而不是为每个 Agent 单独设置。因此:
|
||||||
|
- Kernel 配置应该优先于 Agent 配置
|
||||||
|
- Agent 配置主要用于存储 personality、system_prompt 等
|
||||||
|
- model 配置应该由全局设置控制
|
||||||
|
|
||||||
|
**验证修复**:
|
||||||
|
1. 在"模型与 API"页面配置新模型
|
||||||
|
2. 发送消息
|
||||||
|
3. 检查终端日志,应显示 `using model=新模型 from kernel config`
|
||||||
|
4. 检查 API 请求体,`model` 字段应为新模型
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 相关文档
|
||||||
|
|
||||||
- [OpenFang 配置指南](./openfang-configuration.md) - 配置文件位置、格式和最佳实践
|
- [OpenFang 配置指南](./openfang-configuration.md) - 配置文件位置、格式和最佳实践
|
||||||
- [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置
|
- [Agent 和 LLM 提供商配置](./agent-provider-config.md) - Agent 管理和 Provider 配置
|
||||||
@@ -815,6 +1012,8 @@ curl http://localhost:1420/api/agents
|
|||||||
|
|
||||||
| 日期 | 变更 |
|
| 日期 | 变更 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| 2026-03-23 | 添加 9.3 节:更换模型配置后仍使用旧模型 - Agent 配置优先于 Kernel 配置导致的问题 |
|
||||||
|
| 2026-03-22 | 添加内核 LLM 响应问题:loop_runner.rs 硬编码模型和响应导致 Coding Plan API 不工作 |
|
||||||
| 2026-03-20 | 添加端口配置问题:runtime-manifest.json 声明 4200 但实际运行 50051 |
|
| 2026-03-20 | 添加端口配置问题:runtime-manifest.json 声明 4200 但实际运行 50051 |
|
||||||
| 2026-03-18 | 添加记忆提取和图谱 UI 问题 |
|
| 2026-03-18 | 添加记忆提取和图谱 UI 问题 |
|
||||||
| 2026-03-18 | 添加刷新后对话丢失问题和 ChatArea 布局问题 |
|
| 2026-03-18 | 添加刷新后对话丢失问题和 ChatArea 布局问题 |
|
||||||
|
|||||||
Reference in New Issue
Block a user