From 01f8dccc0681e841002291423f318263d4c8ff93 Mon Sep 17 00:00:00 2001 From: Val-sss <154882199@qq.com> Date: Sun, 10 May 2026 15:36:44 +0800 Subject: [PATCH] =?UTF-8?q?release:=20v2.0.0=20=E2=80=94=20Eval=E9=A9=B1?= =?UTF-8?q?=E5=8A=A8=E6=9E=B6=E6=9E=84=E5=8D=87=E7=BA=A7=EF=BC=8C=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=87=8F=E5=8C=96=E9=AA=8C=E8=AF=81=E6=AF=8F=E4=B8=AA?= =?UTF-8?q?=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心成果:Eval 0%→20% (3/15 PASS),总测试通过数 294→377 (+28%) 新增: - usage_finder: AST确定性调用关系传递 - bug_decomposed: 多bug任务逐TestClass拆解修复 - 新文件创建: _maybe_create_missing_module - 工程约束注入: _detect_engineering_hints - 自适应采样: 大文件1次/小文件3次 - bench诊断报告系统: bench_diagnose.py 实验验证(数据说话): - diff格式对32B弊大于利(超时),已回滚 - 自适应采样解决超时但降低多样性 - usage_finder/bug_decomposed机制有效但受模型能力限制 Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 35 ++++++++++++++++++++ README.md | 7 ++-- kaiwu/__init__.py | 2 +- kaiwu/core/model_capability.py | 2 +- kaiwu/core/test_parser.py | 60 +++++++++++++++++++++++++++++++--- kaiwu/experts/generator.py | 53 ++++++++++++++++++++++++++++-- kaiwu/llm/llama_backend.py | 36 +++++++++++++++++--- pyproject.toml | 2 +- 8 files changed, 181 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31af89e..e4c635d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,41 @@ All notable changes to KWCode are documented here. --- +## [2.0.0] - 2026-05-10 + +### Eval驱动架构升级:数据说话,量化验证每个改动 + +**核心成果**:Eval通过率 0%→20%(3/15 PASS),总测试通过数 294→377(+83, +28%) + +#### Added + +- **usage_finder调用关系传递**(`kaiwu/core/usage_finder.py`):AST确定性找到所有函数调用点,注入prompt让LLM知道修改函数签名时需要同步更新哪些调用方 +- **多bug逐类拆解**(`_run_bug_decomposed()`):多bug任务按TestClass分组,每组独立LLM调用+验证,累积修复不退步。解决"200行14个bug一次全修"的工作记忆溢出问题 +- **新文件创建**(`_maybe_create_missing_module()`):检测ModuleNotFoundError/ImportError,自动创建缺失模块文件并加入locator处理队列 +- **工程约束注入**(`_detect_engineering_hints()`):检测通用工程模式(循环引用保护、递归深度限制、短路求值)并注入prompt,不是背题而是通用约束 +- **自适应采样**:大文件(>150行)只采1次(temp=0.0)避免超时,小文件保持3次异构采样(0.0/0.2/0.4) +- **bench诊断报告系统**(`bench_diagnose.py`):从DetailedLogger日志中提取"脉搏"级诊断——attempt delta、prompt注入检测、机制触发统计、时间分布、LLM输出质量 + +#### Changed + +- **targeted_fix恢复whole_file模式**:diff格式实验证明对32B模型弊大于利(算行号太慢导致超时),回滚为whole_file输出 +- **异构采样自适应**:根据文件大小动态选择采样策略,大文件省时间给retry + +#### Removed + +- 3个过时的search触发测试(search机制已在v1.8中移除,测试未同步清理) + +#### Eval实验记录 + +| 实验 | 假设 | 结果 | 结论 | +|------|------|------|------| +| unified diff格式 | 减少token提高精度 | t21超时,t05/t13持平 | 32B算行号太慢,已回滚 | +| 自适应采样 | 大文件降低采样避免超时 | t21不超时但41/49(退步) | 采样次数↓=尝试多样性↓ | +| usage_finder | 调用关系帮助rename | t08仍0/26 | LLM仍不会全局rename | +| bug_decomposed | 逐类修复提高命中 | t15有ctx bug导致退步 | bug已修,机制本身有效(52/54) | + +--- + ## [1.9.0] - 2026-05-09 ### 9项架构优化:从"LLM看不懂反馈"到"工程替LLM消化反馈" diff --git a/README.md b/README.md index 3151770..e28d43d 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,13 @@ [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://python.org) [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Mac%20%7C%20Linux-lightgrey.svg)]() [![Multi-Platform Tests](https://github.com/val1813/kwcode/actions/workflows/test.yml/badge.svg)](https://github.com/val1813/kwcode/actions/workflows/test.yml) -[![Version](https://img.shields.io/badge/Version-1.7.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.0.0-blue.svg)]() --- -> **v1.7.0 已发布!** KAIJU架构借鉴:Generator bounded context + 存根任务sub-task decomposition + DetailedLogger完整流水线日志。安装命令: +> **v2.0.0 已发布!** Eval驱动架构升级:自适应采样 + 调用关系传递(usage_finder) + 多bug逐类拆解(bug_decomposed) + 新文件创建 + 工程约束注入 + bench诊断报告系统。Eval通过率 0%→20%(3/15 PASS),总测试通过数 294→377(+28%)。安装命令: > > ```bash > pip install kwcode @@ -30,6 +30,9 @@ | 日期 | 内容 | |------|------| +| 05-09 | **v2.0.0** Eval驱动架构升级:自适应采样(大文件1次/小文件3次) + usage_finder调用关系传递 + bug_decomposed多bug逐类拆解 + 新文件创建(_maybe_create_missing_module) + 工程约束注入(_detect_engineering_hints) + bench诊断报告系统(bench_diagnose.py) + 3个失败测试清理 + Eval 0%→20%(3/15 PASS, +83 tests) | +| 05-08 | **v1.9.0** 工程消化反馈:execution_feedback内循环(改完立即跑测试看结果) + 小模型自适应(8b以下只采1次) + 结构化诊断句生成(generate_diagnosis) + 精确失败归因(attribute_failures_to_files) | +| 05-07 | **v1.8.0** 21项修复:whole_file_refactor + targeted_fix + fault_functions定位 + regression_guard + checkpoint不覆盖 + syntax免费重试 + error[-3000:]取末尾 + CJK截断 | | 05-07 | **v1.7.0** KAIJU架构借鉴:Generator bounded context(只传当前函数+相关测试) + 存根任务sub-task decomposition(逐函数独立实现) + DetailedLogger完整流水线日志(不截断LLM输入输出) + OpenAI兼容API检测修复(kaiwu部署器兼容) | | 05-07 | **v1.6.2** 执行反馈深度升级:结构化测试失败解析(parse_test_failures) + TraceCoder历史教训累积(attempt_history) + whole_file写入修复(存根任务不再patches=0) + 完整审计日志(llm_calls/node_io) + pytest -v详细输出 | | 05-07 | **v1.6.1** 架构收敛:删除WholeFileImplExpert/DependencyFixExpert,纯确定性机制驱动pipeline。Generator增强(upstream_constraints注入system prompt + retry_hint携带上次代码 + tier=small填空框架)。License改为Apache-2.0。513 tests green | diff --git a/kaiwu/__init__.py b/kaiwu/__init__.py index ea8d817..cb85d90 100644 --- a/kaiwu/__init__.py +++ b/kaiwu/__init__.py @@ -2,4 +2,4 @@ try: from importlib.metadata import version as _pkg_version __version__ = _pkg_version("kwcode") except Exception: - __version__ = "1.8.0" + __version__ = "2.0.0" diff --git a/kaiwu/core/model_capability.py b/kaiwu/core/model_capability.py index ae760a7..822783b 100644 --- a/kaiwu/core/model_capability.py +++ b/kaiwu/core/model_capability.py @@ -37,7 +37,7 @@ STRATEGIES = { force_plan_mode=True, max_files_per_task=2, max_functions_per_task=5, - max_retries=3, + max_retries=1, search_trigger_after=1, complexity_warning_threshold=2, ), diff --git a/kaiwu/core/test_parser.py b/kaiwu/core/test_parser.py index 43a9b63..a00cee2 100644 --- a/kaiwu/core/test_parser.py +++ b/kaiwu/core/test_parser.py @@ -355,10 +355,16 @@ def generate_diagnosis(structured_failures: list[dict]) -> str: loc = f" ({file}:{line_no})" if file and line_no else "" if error_type == 'AssertionError' and expected and actual: - # 最精确的情况:知道期望和实际 - lines.append( - f"- {name}{loc}: 应该返回 {expected},实际返回 {actual}" - ) + # 最精确的情况:知道期望和实际,尝试推断根因 + root_cause = _infer_root_cause(expected, actual, name) + if root_cause: + lines.append( + f"- {name}{loc}: 应该返回 {expected},实际返回 {actual}({root_cause})" + ) + else: + lines.append( + f"- {name}{loc}: 应该返回 {expected},实际返回 {actual}" + ) elif error_type == 'AttributeError': lines.append( f"- {name}{loc}: {snippet[:120]}(对象缺少该属性或方法)" @@ -399,3 +405,49 @@ def generate_diagnosis(structured_failures: list[dict]) -> str: lines.append(f"- {name}: 测试失败(无详细信息)") return "\n".join(lines) + + +def _infer_root_cause(expected: str, actual: str, test_name: str) -> str: + """ + 从expected/actual的差异推断根因。确定性工程逻辑,不是写死答案。 + 返回空字符串表示无法推断。 + """ + exp_lower = expected.lower() + act_lower = actual.lower() + name_lower = test_name.lower() + + # 转义字符相关:actual中有反斜杠但expected中没有,或反之 + if '\\' in actual and '\\' not in expected: + return "转义字符未被正确处理,反斜杠应该触发转义逻辑" + if '\\' in expected and '\\' not in actual: + return "反斜杠被错误消耗,转义后的字面量丢失" + + # 引号相关:expected有引号但actual在引号处截断 + if ('"' in expected or "'" in expected) and len(actual) < len(expected): + if 'escape' in name_lower or 'quote' in name_lower: + return "引号转义未处理,遇到转义引号时应继续解析而非结束字符串" + + # 负数/符号相关:符号相反 + try: + exp_num = float(expected) + act_num = float(actual) + if exp_num == -act_num: + return "符号取反错误,检查负号/unary minus的处理逻辑" + if exp_num < 0 and act_num > 0: + return "负数未被正确处理,需要支持unary minus" + except (ValueError, TypeError): + pass + + # None vs 有值:函数返回了None + if act_lower in ('none', 'null') and exp_lower not in ('none', 'null'): + return "函数返回了None,可能缺少return语句或逻辑分支未覆盖" + + # 空列表/空字符串 vs 有内容 + if act_lower in ('[]', '""', "''", '{}') and exp_lower not in ('[]', '""', "''", '{}'): + return "返回了空结果,核心逻辑可能未执行" + + # 截断:actual是expected的前缀 + if expected.startswith(actual) and len(actual) < len(expected): + return "结果被截断,解析/处理提前终止了" + + return "" diff --git a/kaiwu/experts/generator.py b/kaiwu/experts/generator.py index 29079f1..b2baa37 100644 --- a/kaiwu/experts/generator.py +++ b/kaiwu/experts/generator.py @@ -1619,10 +1619,15 @@ class GeneratorExpert: continue # 自适应:大文件只采1次,避免超时 + # 8b及以下模型:统一只采1次(reasoning token消耗大,多采样会超时) file_lines = len(content.split('\n')) - if file_lines > 150: + is_small_model = hasattr(self.llm, 'ollama_model') and any( + s in getattr(self.llm, 'ollama_model', '').lower() + for s in ('1b', '3b', '4b', '7b', '8b') + ) + if is_small_model or file_lines > 150: max_tokens = 2048 - temperatures = [0.0] # 大文件只采1次,省时间给retry + temperatures = [0.0] # 小模型/大文件只采1次 else: max_tokens = 4096 temperatures = [0.0, 0.2, 0.4] @@ -1802,7 +1807,11 @@ class GeneratorExpert: # 选出最佳候选后,立刻运行failing tests看结果 # 如果还有失败,把结构化诊断给LLM再生成一次(最多1轮额外尝试) # 解决阻碍3(改完不知道结果)和阻碍4(每次从同一起点出发) - code = self._run_execution_feedback(ctx, fpath, content, code, system, max_tokens) + # 小模型用轻量版(timeout短、只跑2个failing tests) + if is_small_model: + code = self._run_execution_feedback_lite(ctx, fpath, content, code, system, max_tokens) + else: + code = self._run_execution_feedback(ctx, fpath, content, code, system, max_tokens) patches.append({ "file": fpath, @@ -1909,6 +1918,44 @@ class GeneratorExpert: self.tools.write_file(fpath, original) return code + def _run_execution_feedback_lite(self, ctx, fpath: str, original: str, code: str, + system: str, max_tokens: int) -> str: + """ + 轻量版Execution Feedback:小模型专用。 + 只跑最多2个failing tests,timeout 15s,不做第二轮LLM生成。 + 目的:快速检测是否引入了新bug,如果引入则回滚。 + """ + if not self.tools: + return code + + # 写入候选代码,跑测试 + self.tools.write_file(fpath, code) + from kaiwu.core.context import TaskContext as _TC + from kaiwu.experts.verifier import VerifierExpert as _VE + _tmp_ctx = _TC(project_root=ctx.project_root) + _tmp_ver = _VE(self.llm, self.tools) + _result = _tmp_ver.run_tests_only(_tmp_ctx) + + test_output = _result.get("output", "") + passed = _result.get("passed", 0) + total = _result.get("total", 0) + pre_passed = getattr(ctx, '_pre_test_passed', 0) + + # 如果全部通过或比初始状态好,直接返回 + if (passed == total and total > 0) or passed > pre_passed: + self.tools.write_file(fpath, original) + return code + + # 如果退步了(比初始状态还差),回滚到原始代码 + if passed < pre_passed: + logger.info("[feedback_lite] 退步(%d < %d),回滚", passed, pre_passed) + self.tools.write_file(fpath, original) + return original # 回滚到原始代码 + + # 持平或无法判断,保留候选 + self.tools.write_file(fpath, original) + return code + def _detect_engineering_hints(self, failing_info: str) -> str: """检测测试失败中的通用工程模式,返回提示。 不是背题——这些是通用的工程约束(递归保护、边界检查等)。""" diff --git a/kaiwu/llm/llama_backend.py b/kaiwu/llm/llama_backend.py index d5a904f..fc79a86 100644 --- a/kaiwu/llm/llama_backend.py +++ b/kaiwu/llm/llama_backend.py @@ -9,6 +9,13 @@ import os import re from typing import Optional +# 在模块加载时设置NO_PROXY,确保httpx不会把localhost请求走系统代理 +# 这必须在import httpx之前或httpx.Client创建之前生效 +_no_proxy = os.environ.get('NO_PROXY', '') +if 'localhost' not in _no_proxy: + os.environ['NO_PROXY'] = f"{_no_proxy},localhost,127.0.0.1" if _no_proxy else "localhost,127.0.0.1" + os.environ['no_proxy'] = os.environ['NO_PROXY'] + import httpx from kaiwu.core.network import is_china_network @@ -36,7 +43,9 @@ class LLMBackend: # Models known to use thinking/reasoning tokens that consume num_predict budget REASONING_PREFIXES = ("deepseek-r1", "qwq", "qwen3", "gemma4") # Multiplier for num_predict when using reasoning models + # 8b模型thinking较短,用3x;大模型用8x REASONING_TOKEN_MULTIPLIER = 8 + REASONING_TOKEN_MULTIPLIER_SMALL = 3 # for <=8b models # ModelScope model mapping for China network auto-switching MODELSCOPE_MODELS = { @@ -68,6 +77,8 @@ class LLMBackend: self._last_elapsed: float = 0.0 # last generate elapsed seconds # Detect if this is an OpenAI-compatible API (not Ollama) self._is_openai_compat = self._detect_openai_compat(ollama_url) + # 对localhost/127.0.0.1的请求创建无代理的httpx client(避免系统代理干扰) + self._http_client = self._create_http_client(ollama_url) # Token budget tracking self._total_input_tokens: int = 0 self._total_output_tokens: int = 0 @@ -98,6 +109,19 @@ class LLMBackend: else: logger.info("LLM backend: Ollama HTTP (%s, model=%s)", self.ollama_url, self.ollama_model) + @staticmethod + def _create_http_client(url: str): + """对localhost请求创建无代理httpx client,避免系统代理干扰ollama调用。""" + import os + url_lower = url.lower() + if "localhost" in url_lower or "127.0.0.1" in url_lower: + # 确保NO_PROXY包含localhost(httpx会读取这个环境变量) + no_proxy = os.environ.get('NO_PROXY', '') + if 'localhost' not in no_proxy: + os.environ['NO_PROXY'] = f"{no_proxy},localhost,127.0.0.1" if no_proxy else "localhost,127.0.0.1" + os.environ['no_proxy'] = os.environ['NO_PROXY'] + return httpx.Client(timeout=600.0) + @staticmethod def _detect_openai_compat(url: str) -> bool: """ @@ -328,7 +352,11 @@ class LLMBackend: effective_temp = temperature if self._is_reasoning: - effective_tokens = max_tokens * self.REASONING_TOKEN_MULTIPLIER + # 8b及以下模型thinking较短,用小multiplier避免超时 + multiplier = self.REASONING_TOKEN_MULTIPLIER + if any(s in self.ollama_model.lower() for s in ('1b', '3b', '4b', '7b', '8b')): + multiplier = self.REASONING_TOKEN_MULTIPLIER_SMALL + effective_tokens = max_tokens * multiplier if temperature == 0.0: effective_temp = 0.01 @@ -347,10 +375,10 @@ class LLMBackend: payload["options"]["stop"] = stop try: - resp = httpx.post( + resp = self._http_client.post( f"{self.ollama_url}/api/chat", json=payload, - timeout=360.0, + timeout=600.0, ) resp.raise_for_status() data = resp.json() @@ -410,7 +438,7 @@ class LLMBackend: payload["stop"] = stop try: - resp = httpx.post(url, json=payload, headers=headers, timeout=360.0) + resp = self._http_client.post(url, json=payload, headers=headers, timeout=360.0) resp.raise_for_status() data = resp.json() choices = data.get("choices", []) diff --git a/pyproject.toml b/pyproject.toml index 9d8d992..1642942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "kwcode" -version = "1.8.0" +version = "2.0.0" description = "KwCode - Local-model coding agent with MoE expert pipeline" requires-python = ">=3.10" readme = "README.md"