当前位置:首页>python>从 0 到 1:用 Python 实现一个本地单进程 Mini Agent

从 0 到 1:用 Python 实现一个本地单进程 Mini Agent

  • 2026-10-11 08:33:36
从 0 到 1:用 Python 实现一个本地单进程 Mini Agent

把一个“会聊天的模型”,做成一个真正能读代码、改代码、做验证的最小 Agent。

一、先说结论:Mini Agent 不是一个大 Prompt

很多人第一次做 Agent,会从一个很自然的想法开始:

“我已经有模型接口了,再写一段很长的 system prompt,让模型自己规划、自己写代码、自己执行,不就行了吗?”

实际做下来,很快就会发现这条路不够。

原因很简单。一个真正能工作的 Coding Agent,解决的不是“怎么让模型多说一点代码”,而是下面这件事:

用户给出任务-> Agent 理解当前工作区-> Agent 读取相关文件-> Agent 决定下一步要做什么-> Agent 修改代码-> Agent 跑验证-> Agent 根据结果继续调整-> Agent 输出带证据的结论

这已经不是一次文本生成,而是一个带状态、带工具、带边界、带验证的执行系统。

所以这篇文章不讲“怎么写一个超级复杂的 Agent 平台”,只讲一件更重要的事:

如何从 0 到 1,用 Python 做出一个最小但真实可用的本地单进程 Mini Agent。

主线很明确:

  • • 语言:Python
  • • 形态:本地单进程 CLI
  • • 目标:先跑通最小闭环,再考虑扩展能力

二、我们到底要实现一个什么样的 Agent

在开始写代码之前,先把目标收紧。

如果目标定义成“支持很多模型、很多工具、很多 UI”,项目很快就会失控。

更好的定义方式是:

一个最小可用的 Mini Agent,应该能够在本地工作区里读取上下文、执行工具、修改代码、做最基本验证,并输出带证据的结果。

也就是说,我们先不追求:

  • • 多 Agent 编排
  • • Web 控制台
  • • 复杂 Planner
  • • Repo 级语义索引
  • • 超长链路自动恢复

我们先追求一个最小闭环。

这个闭环可以浓缩成一句话:

读 -> 改 -> 验 -> 报

只要这四步跑通,Agent 就成立了。

三、为什么选择 Python + 本地单进程 CLI

这是一个非常务实的选择。

原因不是因为它最炫,而是因为它最适合从 0 到 1:

  • • Python 足够快地把原型跑起来
  • • 单进程让调试最简单
  • • CLI 是最自然的 Agent 入口
  • • 没有服务编排负担
  • • 没有前后端分离负担
  • • 更容易把注意力放在 Agent 内核,而不是外围系统

如果你一开始就做成:

  • • Web 服务
  • • 多 worker 调度
  • • 任务队列
  • • 子 Agent 分发
  • • 浏览器控制台

那么你大概率会先陷入系统复杂度,而不是先把 Agent 跑通。

所以从工程角度看,Python + 本地单进程 CLI 是最合理的起步路线。

四、先搭一个最小架构,不要写成一坨脚本

虽然我们做的是 Mini Agent,但它也不应该是一团揉在一起的代码。

更好的最小结构是这样:

CLI  -> Runner      -> Prompt Builder      -> Model Client      -> Tool Registry      -> Tool Executor      -> Approval Policy      -> Memory Store      -> Finalizer      -> Run State

把它翻译成人话,大概就是:

  • • cli.py:负责接收用户输入,启动程序
  • • runner.py:负责 Agent 主循环
  • • prompts.py:负责拼接系统提示词和上下文
  • • llm/client.py:负责调用模型
  • • tools/:负责具体动作
  • • approval/:负责边界和审批
  • • memory/:负责加载规则和历史摘要
  • • verify/finalizer.py:负责把结果整理成可读输出
  • • state.py:负责记录运行状态

这个结构的价值非常大:

  1. 1. 你可以逐步实现,而不是一口气写完
  2. 2. 后面增强能力时,不需要推翻已有系统

五、第一步:先让 Agent Loop 跑起来

一个 Agent 的核心不在 CLI,而在 Runner。

你可以把 Runner 理解成一个很小的状态机:

  1. 1. 构造消息
  2. 2. 调模型
  3. 3. 看模型返回的是“回答”还是“工具调用”
  4. 4. 如果是工具调用,就执行工具
  5. 5. 把工具结果追加回上下文
  6. 6. 继续下一轮
  7. 7. 直到完成或到达上限

这就是 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

这里最值得注意的不是字段多少,而是这个思想:

  • • Agent 不是无状态聊天
  • • Agent 必须知道自己执行过什么动作
  • • Agent 必须知道自己是否修改过代码
  • • Agent 必须知道修改之后是否做过验证

然后再看 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

这其实在工程上做了一件非常重要的事:

它把模型每一轮的选择限制成两种:

  1. 1. 给出最终文本
  2. 2. 发起一个工具调用

对于一个 Mini Agent,这已经足够。

七、第三步:给 Agent 装上眼睛和手

一个最小 Coding Agent,并不需要很多工具。

如果目标只是跑通最小编码闭环,我建议工具集先收敛成这几类:

  • • list_files
  • • read_file
  • • write_file
  • • apply_patch
  • • run_command

如果还需要额外信息,再加:

  • • web_search

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. 再让它有“写”的能力

最直接的写入方式当然是:

  • • write_file

但如果只有这个工具,模型很容易整文件重写。对于真实工程,这样很危险。

所以我们又给它补了一个更适合代码修改场景的工具:

  • • apply_patch

当前项目里的 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",    )

也就是说,系统会先判断某个动作属于:

  • • allow
  • • ask
  • • deny

然后工具层再做二次校验。

这两层解决的是不同问题:

  • • policy 决定“从系统视角是否允许这类动作”
  • • tool 决定“这个具体参数是否合法”

这是一种非常值得保留的工程模式。

十、第六步:让 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 and verified
  • • 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 读取:

  • • provider 配置
  • • 模型配置
  • • 本地行为开关

例如:

MINI_AGENT_AUTO_APPROVE_WRITES=true

这意味着写代码动作可以自动批准,而运行命令仍然保留人工边界。

十三、我们是怎么一步步把这个项目做出来的

如果回头看整个实现过程,这个 Mini Agent 不是一口气写出来的,而是按最小风险路径一步步搭起来的。

大致顺序是这样的:

第一步:先把最小 Loop 跑起来

只实现:

  • • RunState
  • • ModelTurn
  • • ToolCall
  • • Runner

目标只有一个:让 Agent 能进入循环。

第二步:加最小读工具

先做:

  • • list_files
  • • read_file

因为 Agent 必须先看见工作区,后面才谈得上写代码。

第三步:加写文件能力

先有:

  • • write_file

再演进到:

  • • apply_patch

这一步让系统从“能生成文件”变成“能更稳地修改文件”。

第四步:加审批和边界

这一步是整个系统开始有“工程感”的分水岭。

Agent 从这里开始不再是一个无边界执行器,而是一个受控系统。

第五步:加写后验证闭环

这一步非常关键。

它决定了系统到底是:

  • • 一个代码生成器

还是:

  • • 一个最小 Coding Agent

第六步:接真实模型

等内核稳定以后,再接 OpenAI-compatible provider。

做到这里你会发现,模型接入本身并不是最难的。

真正困难的是前面的:

  • • 工具协议
  • • 状态管理
  • • 边界控制
  • • 结果验证

第七步:补 CLI 体验

最后再去做:

  • • --chat
  • • spinner
  • • .env.local
  • • 自动批准写入

这些不是 Agent 的核心,但它们决定了系统能不能被真正长期使用。

十四、当前这个 Mini Agent 已经具备什么能力

如果你现在把这个项目跑起来,它已经能完成很多非常真实的本地任务。

包括:

  • • 以本地单进程 CLI 方式运行
  • • 接入真实 OpenAI-compatible 模型
  • • 查看工作区文件和目录
  • • 在工作区内写文件和打补丁
  • • 在受控边界内执行验证命令
  • • 做多轮 chat
  • • 显示工作中的 spinner
  • • 从 .env.local 读取本地配置
  • • 根据证据输出最终结果
  • • 区分“已验证完成”和“未验证完成”

对于一个从 0 到 1 的 Mini Agent 来说,这已经是一个非常完整的 MVP。

十五、如果继续往前走,一个完整 Agent 还应该具备什么能力

当前这个版本已经足够帮助你理解和实践最小 Agent,但如果你想继续演进,一个更完整的 Agent 还应该逐步具备这些能力:

1. Planning

现在的 Runner 还是“边想边做”。

更完整的 Agent 应该先产出一个小计划,再执行。

2. Scratchpad

需要显式的短期任务状态,例如:

  • • 当前子任务
  • • 已完成步骤
  • • 当前阻塞
  • • 待验证项

3. 更强的 Repo Context

现在主要还是依赖:

  • • list_files
  • • read_file

后续如果要支持更大项目,需要逐步引入:

  • • 检索
  • • AST 粒度切分
  • • 依赖关系
  • • 增量索引

4. 更细的失败处理

不仅要知道失败了,还要知道失败属于:

  • • 代码问题
  • • 环境问题
  • • 测试问题
  • • 工具问题

5. 轨迹可视化

更完整的 Agent 应该能告诉用户:

  • • 每一步做了什么
  • • 为什么这么做
  • • 当前停在哪一步

6. 更强验证能力

现在验证主要还是命令级。

更完整的版本可能还需要:

  • • 浏览器验证
  • • 接口验证
  • • 截图比较
  • • 端到端测试

十六、最后总结

从 0 到 1 实现一个 Mini Agent,不需要一开始就做成复杂平台。

真正正确的顺序是:

  1. 1. 先定义最小闭环
  2. 2. 先把 Agent Loop 跑通
  3. 3. 再补最小工具集
  4. 4. 尽早加入工程边界
  5. 5. 再把写后验证闭环做起来
  6. 6. 最后再补本地交互体验

如果只记住一句话,我希望是这句:

Mini Agent 不是一个“会生成代码的聊天机器人”,而是一个“能在受控环境里读取上下文、执行动作、修改代码、做验证并输出证据”的最小执行系统。

当你把这件事做对了,后面的 Planner、Scratchpad、Repo 索引,甚至多 Agent,才有继续演进的基础。

最新文章

随机文章