把一个“会聊天的模型”,做成一个真正能读代码、改代码、做验证的最小 Agent。
一、先说结论:Mini Agent 不是一个大 Prompt
很多人第一次做 Agent,会从一个很自然的想法开始:
“我已经有模型接口了,再写一段很长的 system prompt,让模型自己规划、自己写代码、自己执行,不就行了吗?”
实际做下来,很快就会发现这条路不够。
原因很简单。一个真正能工作的 Coding Agent,解决的不是“怎么让模型多说一点代码”,而是下面这件事:
用户给出任务-> Agent 理解当前工作区-> Agent 读取相关文件-> Agent 决定下一步要做什么-> Agent 修改代码-> Agent 跑验证-> Agent 根据结果继续调整-> Agent 输出带证据的结论
这已经不是一次文本生成,而是一个带状态、带工具、带边界、带验证的执行系统。
所以这篇文章不讲“怎么写一个超级复杂的 Agent 平台”,只讲一件更重要的事:
如何从 0 到 1,用 Python 做出一个最小但真实可用的本地单进程 Mini Agent。
主线很明确:
二、我们到底要实现一个什么样的 Agent
在开始写代码之前,先把目标收紧。
如果目标定义成“支持很多模型、很多工具、很多 UI”,项目很快就会失控。
更好的定义方式是:
一个最小可用的 Mini Agent,应该能够在本地工作区里读取上下文、执行工具、修改代码、做最基本验证,并输出带证据的结果。
也就是说,我们先不追求:
我们先追求一个最小闭环。
这个闭环可以浓缩成一句话:
读 -> 改 -> 验 -> 报
只要这四步跑通,Agent 就成立了。
三、为什么选择 Python + 本地单进程 CLI
这是一个非常务实的选择。
原因不是因为它最炫,而是因为它最适合从 0 到 1:
- • 更容易把注意力放在 Agent 内核,而不是外围系统
如果你一开始就做成:
那么你大概率会先陷入系统复杂度,而不是先把 Agent 跑通。
所以从工程角度看,Python + 本地单进程 CLI 是最合理的起步路线。
四、先搭一个最小架构,不要写成一坨脚本
虽然我们做的是 Mini Agent,但它也不应该是一团揉在一起的代码。
更好的最小结构是这样:
CLI -> Runner -> Prompt Builder -> Model Client -> Tool Registry -> Tool Executor -> Approval Policy -> Memory Store -> Finalizer -> Run State
把它翻译成人话,大概就是:
- •
prompts.py:负责拼接系统提示词和上下文 - •
verify/finalizer.py:负责把结果整理成可读输出
这个结构的价值非常大:
五、第一步:先让 Agent Loop 跑起来
一个 Agent 的核心不在 CLI,而在 Runner。
你可以把 Runner 理解成一个很小的状态机:
这就是 Agent 的最小循环。
当前实现里的运行状态定义在 mini_agent/state.py:
from dataclasses import dataclass, fieldfrom typing importAny, Literal@dataclassclassToolObservation: tool_name: str args: dict[str, Any] ok: bool output: str metadata: dict[str, Any] = field(default_factory=dict)@dataclassclassRunState: task: str step_count: int = 0 tool_calls: int = 0 status: Literal["running", "finished", "stopped", "error"] = "running" stop_reason: str | None = None observations: list[ToolObservation] = field(default_factory=list) modified_files: list[str] = field(default_factory=list) verification_commands: list[str] = field(default_factory=list) verification_passed: bool | None = None
这里最值得注意的不是字段多少,而是这个思想:
然后再看 mini_agent/runner.py 中最核心的主循环:
while state.step_count < self.max_steps: state.step_count += 1 turn = self.model_client.complete(messages, self._tool_schemas())if turn.tool_call isNone: state.status = "finished" answer = self.finalizer.build_answer(turn.text or"", state)self.memory_store.append_session_summary(answer)return answer decision = self.approval_policy.check( turn.tool_call.name, turn.tool_call.arguments, )if decision.action == "deny": observation = self.tool_executor.denied(turn.tool_call)elif decision.action == "ask": ...else: observation = self.tool_executor.execute(turn.tool_call) state.tool_calls += 1 state.observations.append(observation)
这段代码,就是整个 Mini Agent 的心脏。
这一阶段最重要的原则
不要一上来就追求复杂规划。
先把最小循环跑通。
只要系统已经满足:
你的 Agent 就已经从“聊天机器人”跨到“执行系统”了。
六、第二步:定义模型输出,不要让它随意发挥
要让 Runner 可控,模型输出就不能是完全自由的文本。
更好的做法是定义一个最小结构化输出。
当前项目在 mini_agent/llm/schemas.py 里用了这样一组结构:
from dataclasses import dataclassfrom typing importAny@dataclassclassToolCall: name: str arguments: dict[str, Any]id: str | None = None@dataclassclassModelTurn: text: str | None = None tool_call: ToolCall | None = None
这其实在工程上做了一件非常重要的事:
它把模型每一轮的选择限制成两种:
对于一个 Mini Agent,这已经足够。
七、第三步:给 Agent 装上眼睛和手
一个最小 Coding Agent,并不需要很多工具。
如果目标只是跑通最小编码闭环,我建议工具集先收敛成这几类:
如果还需要额外信息,再加:
1. 先让它看得见
任何写代码动作之前,Agent 都必须先理解当前工作区。
所以最先做的两个工具应该是:
当前实现里的最小版本在 mini_agent/tools/builtins.py:
classListFilesTool:defrun(self, arguments: dict) -> str: path = ensure_within_workspace(self.workspace, self.workspace / arguments["path"]) items = sorted(p.name for p in path.iterdir())[: self.max_entries]return"\n".join(items)classReadFileTool:defrun(self, arguments: dict) -> str: path = ensure_within_workspace(self.workspace, self.workspace / arguments["path"])return path.read_text(encoding="utf-8")[: self.max_bytes]
这一步非常像给 Agent 装上眼睛。
2. 再让它有“写”的能力
最直接的写入方式当然是:
但如果只有这个工具,模型很容易整文件重写。对于真实工程,这样很危险。
所以我们又给它补了一个更适合代码修改场景的工具:
当前项目里的 ApplyPatchTool 不是复杂的 unified diff 解析器,而是一个很适合 MVP 的版本:
classApplyPatchTool:defrun(self, arguments: dict) -> str: path = ensure_within_workspace(self.workspace, self.workspace / arguments["path"]) old = arguments["old"] new = arguments["new"] replace_all = arguments.get("replace_all", False) ... text = path.read_text(encoding="utf-8") match_count = text.count(old) ... updated = text.replace(old, new, 1) path.write_text(updated, encoding="utf-8")returnf"Patched file: {arguments['path']}\nReplacements: {replacements}"
这个实现为什么适合最小 Agent?
因为它有几个非常实际的优点:
这一步可以理解成给 Agent 装上了手。
八、第四步:不要急着给自由 Shell,先做工程边界
很多人做 Agent 时最容易踩的坑,就是太早给模型自由命令执行能力。
从“演示效果”看,自由 shell 很酷。
但从工程角度看,这通常意味着:
所以在这个项目里,我们做了一个非常重要的选择:
run_command 不是开放 shell,而是一个受控验证工具。
当前实现中的 RunCommandTool 只允许 pytest 风格命令:
classRunCommandTool: ALLOW_COMMAND_PREFIXES = ("pytest", "python -m pytest", "python3 -m pytest") DENY_COMMAND_PREFIXES = ("rm ", "del ", "rmdir ", "shutdown", "reboot", "curl |", "wget |")
这意味着 Agent 的第一职责被严格收敛成:
而不是变成任意命令执行器。
九、第五步:把审批层和工具层分开
当你给 Agent 加边界时,最好不要只做一层。
更稳的做法是把“审批策略”和“工具校验”拆开。
当前项目在 mini_agent/approval/policy.py 里定义了审批策略:
classDefaultApprovalPolicy: READ_ONLY_TOOLS = {"list_files", "read_file", "web_search"} ASK_TOOLS = {"write_file", "apply_patch"} ALLOW_COMMAND_PREFIXES = ("pytest","python -m pytest","python3 -m pytest", )
也就是说,系统会先判断某个动作属于:
然后工具层再做二次校验。
这两层解决的是不同问题:
- •
policy 决定“从系统视角是否允许这类动作”
这是一种非常值得保留的工程模式。
十、第六步:让 Agent 不只是“会写”,而是“会完成代码任务”
一个真正的 Coding Agent,不能只停留在“把文件写出来”。
它必须知道:
这一点在当前项目里,是通过 RunState + Runner 完成的。
例如在 mini_agent/runner.py 中,我们有这样一段状态更新逻辑:
def_record_observation_effects(self, state: RunState, tool_name: str, arguments: dict, output: str) -> None:if tool_name in {"write_file", "apply_patch"}: path = arguments.get("path")ifisinstance(path, str) and path notin state.modified_files: state.modified_files.append(path) state.verification_passed = Noneif tool_name == "run_command": command = arguments.get("command")ifisinstance(command, str): state.verification_commands.append(command) exit_code = self._extract_exit_code(output)if exit_code isnotNone: state.verification_passed = exit_code == 0
这段代码其实非常关键,因为它把“写代码”从一次文本生成,变成了一个有状态的工程过程。
也正因为如此,最终系统才能区分:
- •
finished but unverified
十一、第七步:最终输出一定要带证据
如果一个 Agent 最后只说:
任务完成了
那它从工程角度几乎没有可信度。
所以我们在 mini_agent/verify/finalizer.py 里做了一件非常重要的事:
把最终结果改成带 evidence 的结果。
例如:
classFinalizer:def_display_status(self, state: RunState) -> str:if state.status != "finished":return state.statusif state.modified_files:if state.verification_passed isTrue:return"finished and verified"return"finished but unverified"return"finished"
然后在 evidence 里追加:
这一步的价值在于,它重新定义了 Agent 的“完成”:
十二、第八步:把它做成一个真正可用的本地 CLI
当核心 Agent Loop 和工具闭环成立之后,最后一步才是把它变成一个可用的产品。
当前项目在 mini_agent/cli.py 里做了几件很实用的事:
1. 支持单次任务模式
例如:
python -m mini_agent.cli --workspace . "read README.md and summarize it"
2. 支持多轮 --chat
聊天模式下,会进入一个 REPL 循环:
defrun_chat_loop( runner, input_func=input, print_func=print, status_stream=None, spinner_interval: float = 0.1,) -> int: ...whileTrue: user_input = input_func("mini-agent> ").strip() ... answer = runner.run(user_input, conversation_history=conversation_history) print_func(answer) conversation_history.append({"role": "user", "content": user_input}) conversation_history.append({"role": "assistant", "content": answer})
这意味着用户看到的是一个多轮 agent,系统内部则仍然保持“一次请求,一次 run”的简单模型。
3. 支持 spinner
如果用户输入之后终端没有反应,会很容易误以为程序卡住了。
所以我们加了一个很轻的工作提示:
defspinner_worker() -> None: frames = "|/-\\" index = 0whilenot stop_spinner.is_set(): frame = frames[index % len(frames)] status_stream.write(f"\rWorking {frame}") ...
从工程角度看,这不是核心能力。
但从产品体验看,它非常重要。
4. 支持 .env.local
这一步也很关键。因为本地使用 Agent 时,如果每次都要手动设置一堆环境变量,会非常难用。
所以当前 CLI 支持从 .env.local 读取:
例如:
MINI_AGENT_AUTO_APPROVE_WRITES=true
这意味着写代码动作可以自动批准,而运行命令仍然保留人工边界。
十三、我们是怎么一步步把这个项目做出来的
如果回头看整个实现过程,这个 Mini Agent 不是一口气写出来的,而是按最小风险路径一步步搭起来的。
大致顺序是这样的:
第一步:先把最小 Loop 跑起来
只实现:
目标只有一个:让 Agent 能进入循环。
第二步:加最小读工具
先做:
因为 Agent 必须先看见工作区,后面才谈得上写代码。
第三步:加写文件能力
先有:
再演进到:
这一步让系统从“能生成文件”变成“能更稳地修改文件”。
第四步:加审批和边界
这一步是整个系统开始有“工程感”的分水岭。
Agent 从这里开始不再是一个无边界执行器,而是一个受控系统。
第五步:加写后验证闭环
这一步非常关键。
它决定了系统到底是:
还是:
第六步:接真实模型
等内核稳定以后,再接 OpenAI-compatible provider。
做到这里你会发现,模型接入本身并不是最难的。
真正困难的是前面的:
第七步:补 CLI 体验
最后再去做:
这些不是 Agent 的核心,但它们决定了系统能不能被真正长期使用。
十四、当前这个 Mini Agent 已经具备什么能力
如果你现在把这个项目跑起来,它已经能完成很多非常真实的本地任务。
包括:
- • 接入真实 OpenAI-compatible 模型
对于一个从 0 到 1 的 Mini Agent 来说,这已经是一个非常完整的 MVP。
十五、如果继续往前走,一个完整 Agent 还应该具备什么能力
当前这个版本已经足够帮助你理解和实践最小 Agent,但如果你想继续演进,一个更完整的 Agent 还应该逐步具备这些能力:
1. Planning
现在的 Runner 还是“边想边做”。
更完整的 Agent 应该先产出一个小计划,再执行。
2. Scratchpad
需要显式的短期任务状态,例如:
3. 更强的 Repo Context
现在主要还是依赖:
后续如果要支持更大项目,需要逐步引入:
4. 更细的失败处理
不仅要知道失败了,还要知道失败属于:
5. 轨迹可视化
更完整的 Agent 应该能告诉用户:
6. 更强验证能力
现在验证主要还是命令级。
更完整的版本可能还需要:
十六、最后总结
从 0 到 1 实现一个 Mini Agent,不需要一开始就做成复杂平台。
真正正确的顺序是:
如果只记住一句话,我希望是这句:
Mini Agent 不是一个“会生成代码的聊天机器人”,而是一个“能在受控环境里读取上下文、执行动作、修改代码、做验证并输出证据”的最小执行系统。
当你把这件事做对了,后面的 Planner、Scratchpad、Repo 索引,甚至多 Agent,才有继续演进的基础。