feat: ctx auto-detect + model-adaptive prompts + P0 bug fixes

Ctx auto-detection (model_capability.get_effective_ctx):
- 4-layer probe: llama.cpp → vLLM → Ollama modelinfo.llama.context_length → offline
- Cloud API (non-localhost) → 128K; known models → exact values; tier → conservative
- User config.yaml ctx field has highest priority
- llama_backend: every Ollama call now includes num_ctx in options
  (kwcode actively sets ctx, not relying on Ollama default 2048)

Model-adaptive prompts:
- SMALL: no tool descriptions (prevents confused tool-call text output),
  strict format (1 func, ≤10 lines, preserve indent, no markdown)
- MEDIUM: concise tool note ("tools auto-called, just output code")
- LARGE: full tool description + auto-call note
- Removed tool descriptions from GENERATOR_PROMPT and TEST_PROMPT templates

P0 bug fixes:
- chat_expert: removed tool descriptions (chat has no tool access)
- debug_subagent: 2x json.loads wrapped in try/except (prevents crash on malformed LLM JSON)
- checkpoint: 2x json.loads wrapped in try/except (prevents crash on damaged manifest)
- _clean_code_output: added cat/echo/touch/hashline-instruction filters

501 tests green, 0 regression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Val-sss
2026-05-06 20:47:16 +08:00
parent 76fa84da08
commit 66ae607d74
6 changed files with 96 additions and 38 deletions

View File

@@ -157,12 +157,17 @@ class Checkpoint:
import json
manifest_path = self._file_backup_dir / "_manifest.json"
if manifest_path.exists():
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
for rel, original_path in manifest.items():
backup_file = self._file_backup_dir / rel
if backup_file.exists():
shutil.copy2(backup_file, original_path)
return True
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
logger.debug("Manifest JSON damaged, falling back to name-based restore")
manifest = None
if manifest:
for rel, original_path in manifest.items():
backup_file = self._file_backup_dir / rel
if backup_file.exists():
shutil.copy2(backup_file, original_path)
return True
# Fallback: simple name-based restore
for f in self._file_backup_dir.rglob("*"):
@@ -201,7 +206,10 @@ def restore_latest() -> bool:
import json
manifest_path = backup_dir / "_manifest.json"
if manifest_path.exists():
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return False
for rel, original_path in manifest.items():
backup_file = backup_dir / rel
if backup_file.exists():

View File

@@ -150,22 +150,49 @@ def _detect_from_name(model_name: str) -> ModelTier:
return ModelTier.MEDIUM
# 已知模型的精确ctx值查不到API时用
_KNOWN_CTX = {
"qwen2.5-coder:7b": 32768, "qwen2.5-coder:14b": 32768, "qwen2.5-coder:32b": 32768,
"qwen3:8b": 32768, "qwen3:14b": 32768, "qwen3:30b-a3b": 32768, "qwen3:72b": 32768,
"deepseek-r1:8b": 65536, "deepseek-r1:14b": 65536, "deepseek-r1:32b": 65536,
"deepseek-r1:70b": 65536,
"gemma3:4b": 8192, "gemma4:e2b": 8192,
"llama3:8b": 8192, "llama3:70b": 8192,
"codellama:7b": 16384, "codellama:13b": 16384, "codellama:34b": 16384,
}
def get_effective_ctx(model_name: str,
ollama_url: str = "http://localhost:11434") -> int:
"""
获取当前模型实际可用的ctx大小。
查询链llama.cpp /props → vLLM /v1/models → Ollama modelinfo → 按tier默认值。
失败全部静默,返回保守默认值。
获取当前模型实际可用的ctx大小。kwcode主动设ctx不依赖用户配置。
查询链(四层,任何层失败静默进下一层):
1. llama.cpp /props → 运行时真实n_ctx
2. vLLM /v1/models → max_model_len
3. Ollama /api/show → modelinfo.llama.context_length原生上限
4. 离线知识库 + 环境判断兜底
返回值会被传给llama_backendOllama调用时自动在options里带num_ctx。
"""
import httpx
# 用户config.yaml手动配了ctx → 最高优先级
try:
from kaiwu.cli.onboarding import load_config
user_ctx = load_config().get("default", {}).get("ctx")
if user_ctx and int(user_ctx) > 0:
return int(user_ctx)
except Exception:
pass
# 1. llama.cpp /props → 运行时真实值,最准
try:
r = httpx.get("http://localhost:8080/props", timeout=2)
if r.status_code == 200:
n_ctx = r.json().get("n_ctx")
if n_ctx and n_ctx > 0:
return int(n_ctx * 0.8)
return int(n_ctx)
except Exception:
pass
@@ -176,11 +203,11 @@ def get_effective_ctx(model_name: str,
if r.status_code == 200:
data = r.json().get("data", [])
if data and "max_model_len" in data[0]:
return int(data[0]["max_model_len"] * 0.8)
return int(data[0]["max_model_len"])
except Exception:
pass
# 3. Ollama /api/show → modelinfo.llama.context_length模型原生上限
# 3. Ollama /api/show → modelinfo.llama.context_length模型权重决定的原生上限)
try:
r = httpx.post(
f"{ollama_url}/api/show",
@@ -191,18 +218,29 @@ def get_effective_ctx(model_name: str,
data = r.json()
native_ctx = data.get("modelinfo", {}).get("llama.context_length", 0)
if native_ctx > 0:
# 原生上限取80%且不超过65536避免本地推理速度
return min(int(native_ctx * 0.8), 65536)
# 原生上限cap到65536超大ctx对本地推理速度影响大
return min(native_ctx, 65536)
except Exception:
pass
# 4. 按tier给保守默认值
# 4. 离线兜底
# 4a. 云API非localhost→ 128K
if "localhost" not in ollama_url and "127.0.0.1" not in ollama_url:
return 131072
# 4b. 已知模型精确值
name_lower = model_name.lower()
if name_lower in _KNOWN_CTX:
return _KNOWN_CTX[name_lower]
# 4c. 按tier给保守默认值
tier = detect_model_tier(model_name, ollama_url)
return {
defaults = {
ModelTier.SMALL: 16384,
ModelTier.MEDIUM: 32768,
ModelTier.LARGE: 65536,
}[tier]
}
return defaults.get(tier, 8192)
def get_strategy(tier: ModelTier) -> ModelStrategy:

View File

@@ -17,15 +17,8 @@ CHAT_SYSTEM = (
"你是KWCode一个本地模型coding agent。"
"用户问非编码问题时≤100字回复≤3句话。"
"自然引导到代码任务。"
"你可以使用以下工具read_file读取文件、write_file写入文件"
"run_bash执行任意shell命令包括ssh、git、pip、curl等"
"你拥有完整的文件系统和命令行访问权限。\n\n"
"如果用户要求连接服务器、查看远程状态、执行运维操作SSH/docker/nginx等"
"请告诉用户具体的命令,并提示可以用 /bash 命令直接执行。例如:\n"
" 连接VPS/bash ssh user@ip -p port\n"
" 查看docker/bash ssh user@ip 'docker ps'\n"
" 重启nginx/bash ssh user@ip 'systemctl restart nginx'\n"
"不要自己执行这些命令,只给出指引让用户确认后执行。"
"如果用户问运维操作SSH/docker/nginx等"
"给出具体命令,提示用 /bash 执行。"
)
CHAT_SEARCH_FAIL_SYSTEM = (

View File

@@ -142,7 +142,10 @@ class DebugSubagent:
json_match = re.search(r'\{[^}]+\}', response)
if not json_match:
return None
strategy = json.loads(json_match.group())
try:
strategy = json.loads(json_match.group())
except (json.JSONDecodeError, ValueError):
return None
# 验证必要字段
if "file" not in strategy or "line" not in strategy:
return None
@@ -201,7 +204,10 @@ class DebugSubagent:
marker = "__DEBUG_JSON__"
if marker in stdout:
json_str = stdout.split(marker)[-1].strip()
return json.loads(json_str)
try:
return json.loads(json_str)
except (json.JSONDecodeError, ValueError):
return None
return None

View File

@@ -97,7 +97,6 @@ _WEB_KEYWORDS = {"html", "css", "web", "网页", "页面", "前端", "界面", "
"website", "网站", "落地页", "登录页", "注册页", "dashboard", "tailwind"}
GENERATOR_PROMPT = """你是代码修复/生成专家。根据任务描述,修改下面的函数代码。
你可以使用以下工具read_file读取文件、write_file写入文件、run_bash执行任意shell命令包括ssh、git、pip等。你拥有完整的文件系统和命令行访问权限。
任务描述:{task_description}
@@ -154,7 +153,6 @@ GENERATOR_NEWFILE_PROMPT = """你是代码生成专家。根据任务描述生
6. 如果没有参考资料或参考资料为空,涉及实时数据(天气、股价、新闻等)时使用占位符如"[数据加载中]",绝对不要编造虚假数据"""
GENERATOR_TEST_PROMPT = """你是测试生成专家。为下面的代码生成 pytest 单元测试。
你可以使用以下工具read_file读取文件、write_file写入文件、run_bash执行任意shell命令。你拥有完整的文件系统和命令行访问权限。
源代码(来自 {source_file}
```
@@ -317,20 +315,29 @@ class GeneratorExpert:
# 模型能力自适应按tier注入不同强度的格式约束
tier = getattr(ctx, 'model_tier', '')
if tier == "small":
# 小模型:不告诉工具存在(避免混乱输出工具调用文本),严格格式约束
system += (
"\n\n## 格式约束(小模型严格执行)\n"
"\n\n## 格式约束(严格执行)\n"
"- 只输出代码,禁止任何解释、注释、命令\n"
"- 每次只修改1个函数修改行数≤10行\n"
"- class内方法必须保持原有缩进通常4空格\n"
"- 直接输出代码,禁止任何解释文字\n"
"- 工具调用之间不超过15个词\n"
"- 禁止输出markdown代码块标记\n"
"- 禁止输出markdown代码块标记```\n"
"- 禁止输出write_file、read_file等命令文本\n"
)
elif tier == "large":
# 大模型:保留工具描述但明确自动调用
system += (
"\n\n## 格式约束\n"
"\n\n## 工具与格式\n"
"- 工具read_file/write_file/run_bash由系统自动调用你只需输出修改后的代码\n"
"- 不要输出工具调用命令\n"
"- 保持代码风格一致,缩进与原文件匹配\n"
)
# medium: 用GENERATOR_BASE_SYSTEM已有的约束即可
else:
# medium: 简洁工具说明
system += (
"\n\n## 工具说明\n"
"- 工具由系统自动调用,你只需输出修改后的代码,不要输出工具调用命令\n"
)
# Append web design rules for HTML/CSS/web tasks
if self._is_web_task(ctx.user_input):
@@ -747,7 +754,11 @@ class GeneratorExpert:
for line in lines:
stripped = line.strip().lower()
# Skip lines that look like tool calls, not file content
if stripped.startswith(("write_file ", "read_file ", "run_bash ", "cd ", "mkdir ")):
if stripped.startswith((
"write_file ", "read_file ", "run_bash ", "cd ", "mkdir ",
"cat ", "echo ", "touch ",
"edit ", "delete ", "insert_after ", # hashline指令残留
)):
continue
cleaned.append(line)
text = "\n".join(cleaned)

View File

@@ -60,6 +60,7 @@ class LLMBackend:
self.ollama_model = ollama_model
self.api_key = api_key
self.verbose = verbose
self._effective_ctx = n_ctx # 主动设ctxOllama调用时自动带num_ctx
self._llm: Optional[object] = None
self._mode = "none"
self._is_reasoning = self._detect_reasoning_model(ollama_model)
@@ -305,6 +306,7 @@ class LLMBackend:
"options": {
"num_predict": effective_tokens,
"temperature": effective_temp,
"num_ctx": self._effective_ctx, # 主动设ctx不让Ollama用默认2048截断
},
}