Harness工程之拆解 AI 编程助手(二):一条指令的完整旅程——从敲回车到修复完成
上篇我们认识了 CoreCoder——一个用 ~1500 行 Python 还原 Claude Code 50 万行源码骨架的开源项目。我们聊了它的 7 面”承重墙”,从搜索替换编辑到子代理隔离,每一面都是 AI Coding Agent 不可或缺的设计。
今天我们动手:跟随一条真实的用户指令——read main.py and fix the broken import,看它从敲下回车到修复完成,系统内部经历了什么。
冷启动:敲下回车前的那几秒
当你输入 corecoder -m gpt-4o,系统做了一连串紧凑的初始化。
用户输入: corecoder -m gpt-4o│▼main.py (3行) ── 调用 cli.main()│▼cli.py: _parse_args() ── 解析 -m, -p, -r, --base-url, --api-key 参数│▼config.py: Config.from_env() ── 加载 .env 环境变量(从 CWD 向上查找到 home 目录)│▼cli.py: 创建 LLM(model, api_key, base_url) ── 初始化 OpenAI 客户端│▼cli.py: 创建 Agent(llm=llm) ── 注册 7 个工具,生成系统提示词│▼cli.py: _repl(agent, config) ── 进入交互循环,等待用户输入
下面逐步拆解每个环节。
第一步:入口(__main__.py)
当你通过 python -m corecoder 或 corecoder 命令启动时,Python 解释器首先执行 __main__.py。这个文件只有 3 行:
corecoder/main.pyfrom corecoder.cli import mainmain()
cli.main(),把控制权交给 CLI 层。第二步:参数解析(cli.py: _parse_args())
main() 的第一个动作是解析命令行参数:
# corecoder/cli.py:23-38def _parse_args():p = argparse.ArgumentParser(prog="corecoder",description="Minimal AI coding agent. Works with any OpenAI-compatible LLM.",)p.add_argument("-m", "--model", help="Model name (default: $CORECODER_MODEL or gpt-4o)")p.add_argument("--base-url", help="API base URL (default: $OPENAI_BASE_URL)")p.add_argument("--api-key", help="API key (default: $OPENAI_API_KEY)")p.add_argument("-p", "--prompt", help="One-shot prompt (non-interactive mode)")p.add_argument("-r", "--resume", metavar="ID", help="Resume a saved session")p.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")return p.parse_args()
-
交互模式(默认):启动后进入 REPL 循环,持续对话
-
单次模式(-p参数):执行一条指令后立即退出,适合脚本集成
第三步:配置加载(config.py: Config.from_env())
# corecoder/config.py:37-55@classmethoddef from_env(cls) -> "Config":# load .env if present (won't override existing env vars)_load_dotenv()# pick up common env vars automaticallyapi_key = (os.getenv("CORECODER_API_KEY")or os.getenv("OPENAI_API_KEY")or os.getenv("DEEPSEEK_API_KEY")or "")return cls(model=os.getenv("CORECODER_MODEL", "gpt-4o"),api_key=api_key,base_url=os.getenv("OPENAI_BASE_URL") or os.getenv("CORECODER_BASE_URL"),...)
# corecoder/config.py:8-25def _load_dotenv():"""Load .env from cwd, walking up to home dir. No-op if python-dotenv missing."""try:from dotenv import load_dotenvenv_path = Path(".env")if not env_path.exists():cur = Path.cwd()home = Path.home()while cur != home and cur != cur.parent:candidate = cur / ".env"if candidate.exists():env_path = candidatebreakcur = cur.parentload_dotenv(env_path, override=False)except ImportError:pass # python-dotenv not installed, silently skip
第四步:创建 LLM 和 Agent
# corecoder/cli.py:69-76llm = LLM(model=config.model,api_key=config.api_key,base_url=config.base_url,temperature=config.temperature,max_tokens=config.max_tokens,)agent = Agent(llm=llm, max_context_tokens=config.max_context_tokens)
# corecoder/agent.py:22-34def __init__(self, llm, tools=None, max_context_tokens=128_000, max_rounds=50):self.llm = llmself.tools = tools if tools is not None else ALL_TOOLS # ① 注册 7 个工具self.messages = [] # ② 初始化空的消息列表self.context = ContextManager(max_tokens=max_context_tokens) # ③ 创建上下文管理器self.max_rounds = max_roundsself._system = system_prompt(self.tools) # ④ 生成系统提示词
第五步:进入 REPL
# corecoder/cli.py:116-126def _repl(agent, config):console.print(Panel(f"[bold]CoreCoder[/bold] v{__version__}\n"f"Model: [cyan]{config.model}[/cyan]"+ (f" Base: [dim]{config.base_url}[/dim]" if config.base_url else "")+ "\nType [bold]/help[/bold] for commands, [bold]Ctrl+C[/bold] to cancel, [bold]quit[/bold] to exit.",border_style="blue",))# ... while True loop waiting for input
主戏:一条指令的三轮循环
read main.py and fix the broken import
用户: "read main.py and fix the broken import"│▼Agent.chat(user_input)① 追加用户消息到 messages 列表② 调用 context.maybe_compress() 检查是否需要压缩上下文│▼┌─────────── 第 1 轮 LLM 调用 ───────────┐│ ││ LLM 看到:用户消息 + 系统提示词 + 工具列表 ││ LLM 思考:我需要先读取文件才能修复 ││ LLM 返回:tool_calls = [read_file(file_path='main.py')]│ │└──────────────────────────────────────────┘│▼Agent._exec_tool() ── 查找 read_file 工具 ── 执行 ── 返回文件内容│▼┌─────────── 第 2 轮 LLM 调用 ───────────┐│ ││ LLM 看到:用户消息 + 工具调用记录 + 文件内容 ││ LLM 思考:我看到了 `from utils import halper`,应该是 helper ││ LLM 返回:tool_calls = [edit_file( ││ file_path='main.py', ││ old_string='from utils import halper',││ new_string='from utils import helper' ││ )] ││ │└──────────────────────────────────────────┘│▼Agent._exec_tool() ── 查找 edit_file 工具 ── 搜索替换 ── 返回 diff│▼┌─────────── 第 3 轮 LLM 调用 ───────────┐│ ││ LLM 看到:用户消息 + 读取结果 + 编辑 diff ││ LLM 思考:编辑成功了,可以告诉用户了 ││ LLM 返回:纯文本 "Fixed: halper → helper." ││ │└──────────────────────────────────────────┘│▼没有 tool_calls → Agent.chat() 返回文本给用户
第一轮:先读后动手
tool_calls = [read_file(file_path='main.py')]
第二轮:发现 Bug,精准修改
tool_calls = [edit_file(file_path='main.py',old_string='from utils import halper',new_string='from utils import helper')]
第三轮:确认完成
Fixed: halper → helper.
代码层面的真实逻辑
# corecoder/agent.py:47-91def chat(self, user_input: str, on_token=None, on_tool=None) -> str:"""Process one user message. May involve multiple LLM/tool rounds."""self.messages.append({"role": "user", "content": user_input})self.context.maybe_compress(self.messages, self.llm)for _ in range(self.max_rounds): # 最多 50 轮循环resp = self.llm.chat( # 调用 LLMmessages=self._full_messages(), # [系统提示词] + [历史消息]tools=self._tool_schemas(), # 7 个工具的 JSON Schemaon_token=on_token, # 流式输出回调)# 没有 tool_calls → LLM 说完了,返回文本if not resp.tool_calls:self.messages.append(resp.message)return resp.content# 有 tool_calls → 执行工具self.messages.append(resp.message)if len(resp.tool_calls) == 1:# 单工具:直接执行tc = resp.tool_calls[0]if on_tool:on_tool(tc.name, tc.arguments)result = self._exec_tool(tc)self.messages.append({"role": "tool","tool_call_id": tc.id,"content": result,})else:# 多工具:并行执行results = self._exec_tools_parallel(resp.tool_calls, on_tool)for tc, result in zip(resp.tool_calls, results):self.messages.append({"role": "tool","tool_call_id": tc.id,"content": result,})# 工具执行后,检查是否需要压缩上下文self.context.maybe_compress(self.messages, self.llm)return "(reached maximum tool-call rounds)"
并行工具执行
# corecoder/agent.py:105-118def _exec_tools_parallel(self, tool_calls, on_tool=None) -> list[str]:"""Run multiple tool calls concurrently using threads.This is inspired by Claude Code's StreamingToolExecutor which startsexecuting tools while the model is still generating. We simplify to:when the model returns N tool calls at once, run them in parallel."""for tc in tool_calls:if on_tool:on_tool(tc.name, tc.arguments)with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:futures = [pool.submit(self._exec_tool, tc) for tc in tool_calls]return [f.result() for f in futures]
系统架构总览图
┌──────────────────────────────────────────────────────────┐│ CLI 层 (cli.py, 280行) ││ ││ REPL 交互循环 · 命令处理 (/help, /save, /compact...) ││ 参数解析 · 输出渲染 (Rich Markdown) · 输入处理 ││ ││ 两种模式: ││ · _repl() → 交互模式,while True 循环 ││ · _run_once() → 单次模式,执行一条指令后退出 │└────────────────────────┬─────────────────────────────────┘│ user_input▼┌──────────────────────────────────────────────────────────┐│ Agent 层 (agent.py, 122行) ││ ││ 消息编排 · 工具调度 · 循环控制 · 并行执行 ││ ││ chat() 核心循环: ││ for _ in range(50): ││ resp = LLM.chat(messages + tools) ││ if no tool_calls → return text ││ else → exec tools (parallel if >1) → append ││ ││ ┌──────────────┐ ┌───────────────────────────────┐ ││ │ Prompt │ │ Context Manager │ ││ │ (prompt.py) │ │ (context.py, 196行) │ ││ │ 33 行 │ │ │ ││ │ │ │ L1: Snip (50% 阈值) │ ││ │ 运行时动态 │ │ → 截断过长的工具输出 │ ││ │ 生成系统提示词│ │ L2: Summarize (70% 阈值) │ ││ │ │ │ → LLM 摘要旧对话 │ ││ │ │ │ L3: Collapse (90% 阈值) │ ││ │ │ │ → 紧急压缩,只保留摘要+最近│ ││ └──────────────┘ └───────────────────────────────┘ ││ ││ ┌──────────────┐ ┌───────────────────────────────┐ ││ │ LLM │ │ Session │ ││ │ (llm.py) │ │ (session.py, 68行) │ ││ │ 199 行 │ │ │ ││ │ │ │ save_session() │ ││ │ · 流式响应 │ │ → JSON → ~/.corecoder/ │ ││ │ · 指数退避重试│ │ load_session() │ ││ │ · 费用估算 │ │ → 恢复消息历史 + 模型 │ ││ │ · 工具调用解析│ │ list_sessions() │ ││ │ │ │ → 列出最近 20 个会话 │ ││ └──────────────┘ └───────────────────────────────┘ │└────────────────────────┬─────────────────────────────────┘│┌─────────────┼─────────────┐▼ ▼ ▼┌────────────┐ ┌────────────┐ ┌────────────┐│ bash │ │ edit │ │ read ││ (115行) │ │ (89行) │ │ (53行) ││ │ │ │ │ ││ Shell 执行 │ │ 搜索替换 │ │ 文件读取 ││ 危险命令拦截│ │ 唯一性校验 │ │ 行号显示 ││ cd 跟踪 │ │ diff 生成 │ │ 分页截断 │├────────────┤ ├────────────┤ ├────────────┤│ write │ │ glob │ │ grep ││ (38行) │ │ (47行) │ │ (78行) ││ │ │ │ │ ││ 文件创建 │ │ 文件名搜索 │ │ 内容搜索 ││ 目录自动创建│ │ 递归匹配 │ │ 正则匹配 │├────────────┤ └────────────┘ └────────────┘│ agent ││ (58行) ││ ││ 子代理生成 ││ 独立上下文 ││ 无递归代理 │└────────────┘Tools Layer(7 个工具, 共 478 行)
各层职责说明
# corecoder/tools/base.pyclass Tool(ABC):"""Minimal tool interface. Subclass this to add new capabilities."""name: strdescription: strparameters: dict # JSON Schema for the function args@abstractmethoddef execute(self, **kwargs) -> str:"""Run the tool and return a text result."""def schema(self) -> dict:"""OpenAI function-calling schema."""return {"type": "function","function": {"name": self.name,"description": self.description,"parameters": self.parameters,},}
数据流总结
用户输入 → [系统提示词 + 历史消息 + 工具列表] → LLM → {工具调用? → 执行 → 循环 | 文本回复? → 返回}
夜雨聆风