mirror of
https://github.com/val1813/kwcode.git
synced 2026-09-03 06:34:30 +08:00
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:
@@ -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():
|
||||
|
||||
@@ -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_backend,Ollama调用时自动在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:
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -60,6 +60,7 @@ class LLMBackend:
|
||||
self.ollama_model = ollama_model
|
||||
self.api_key = api_key
|
||||
self.verbose = verbose
|
||||
self._effective_ctx = n_ctx # 主动设ctx,Ollama调用时自动带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截断
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user