AI面包君Learn · Build · Share
01Bread Agent · 第 6
总进度 6 / 33

多文件重构 + 系统提示词模板

让模型开始动手。多文件重构 + 系统提示词模板,边读边运行配套 Python 代码。

2026-09-0740 分钟340 行代码难度 ★★

1. 故事:代码长大了,要分家

ch04 的 main.py 已经 270 行。所有东西堆在一个文件——工具函数、工具 schema、流式逻辑、REPL、system prompt……找一段代码要拖拉滚动半天。

这一章不引入任何新功能,只做两件事:

  1. 拆成 3 个文件tools.py / agent.py / main.py
  2. 系统提示词模板化:不再是写死的字符串,而是根据"当前可用工具 / cwd / 今天日期"动态拼装

为什么这件事值得单独一章?

  • 第一次看到 class Agent —— 工业级 agent 的标准形态
  • 第一次理解"模块分工"——main.py 应该只剩 REPL 外壳
  • 第一次见到"system prompt 是个函数,不是个字符串"——pi-main 的 system-prompt.ts 也是这个思想

后面 3 章(ch06/ch07/ch08)的代码骨架都建立在本章之上


2. 跑起来

cd ch05_promptpl
python main.py

体验跟 ch04 应该完全一样(除了启动消息换了句话)。

Bread Agent · v5(已重构)
输入消息开始对话;Ctrl+C 退出。

你 > 今天几号?
AI > 今天是 **2026年6月6日**。

你 > 你可以使用哪些工具
AI > 我目前可以使用以下 4 个工具:

1. **read_file** — 读取文本文件内容
2. **write_file** — 写入文本文件(会覆盖已有内容)
3. **list_dir** — 列出目录下的文件和子目录
4. **run_shell** — 执行 shell 命令(危险操作需你确认)

简单说就是:**看文件、写文件、列目录、跑命令**。需要我帮你做什么?

你 > 你当前的工作路径是?
AI > 当前工作目录是:

**`D:\Project\bread agent\bread-agent\ch05_promptpl`**

需要我列一下这个目录里有什么吗?

关键观察:问"今天几号",AI 不需要调任何工具就能答——因为我们在 system prompt 里注入了当前日期。这就是本章新加的能力。

image-20260606143012034


3. 逐行精讲

文件结构

ch05_promptpl/
├── main.py     # REPL 外壳(约 35 行)
├── agent.py    # Agent 类 + build_system_prompt(约 130 行)
├── tools.py    # 工具实现 + schema(约 80 行)
└── README.md

拆分对照表(ch04 → ch05)

原本在 ch04 main.py 哪现在在哪
read_file / write_file / list_dir / run_shell 函数tools.py
TOOLS_SCHEMA / TOOLS_IMPL dicttools.py
stream_one_turn / non_stream_one_turn 函数Agent._stream_one / Agent._non_stream_one 方法
run_turn(messages)Agent.chat(user_input) 方法
写死的 system 消息字符串build_system_prompt() 函数
messages = [...] 在 main 里定义Agent.__init__ 里维护

class Agent 的样子

class Agent:
    MAX_STEPS = 10

    def __init__(self):
        self.client = OpenAI(api_key=..., base_url=...)
        self.model = os.environ["MODEL"]
        self.messages = [{"role": "system", "content": build_system_prompt()}]

    def _stream_one(self):
        ...  # ch04 的 stream_one_turn 改成 method

    def _non_stream_one(self):
        ...  # ch04 的 non_stream_one_turn 改成 method

    def _execute_tool(self, call):
        ...  # 把工具执行抽成 method

    def chat(self, user_input: str):
        self.messages.append({"role": "user", "content": user_input})
        for _ in range(self.MAX_STEPS):
            ...  # ch04 的 run_turn 改成 method

class 的好处

  • messagesclientmodel 这些状态都变成 self.xxx,不用四处传参
  • 外部用起来超清爽:agent = Agent(); agent.chat("hello")
  • 后面 ch06/ch07/ch08 加 hooks/session 时,只需要给构造函数加参数

系统提示词模板化 ★

我们的实现:

DEFAULT_PERSONA = """你是 Bread Agent,一个能操作文件系统和 shell 的中文助理。
工作风格:
- 必要时调用工具;不要凭空捏造文件内容
- 执行危险命令前用户会被询问,被拒绝是正常情况,礼貌改口即可
- 简洁回答;长任务结束后用一句话总结你做了什么
"""

def build_system_prompt(tools_schema, cwd=None, today=None, persona=DEFAULT_PERSONA):
    cwd = cwd or str(Path.cwd())
    today = today or date.today().isoformat()

    tool_lines = [f"- {t['function']['name']}: {t['function']['description']}" for t in tools_schema]
    tools_block = "\n".join(tool_lines)

    return f"""{persona}
# 可用工具
{tools_block}

# 上下文
当前工作目录: {cwd}
今天日期: {today}
"""

为什么要这样拼装?

注入的内容解决的问题
persona角色设定("你是 Bread Agent...")
工具清单模型在 prompt 里就看到工具用途——比单纯 tools= 参数效果更好
当前 cwdagent 知道自己在哪儿,相对路径才有意义
今天日期模型不需要调工具就能答"今天几号"

tools 参数本身已经包含工具描述,但 LLM 实际上对写在 system prompt 里的内容更敏感。所以工业级 agent 都会把工具清单同时塞进 prompt


4. 卡住了怎么办

ImportError: cannot import name 'Agent' from 'agent'

agent.py 没找到。确认你 cd 到了 ch05_promptpl/ 目录里,且 agent.py 文件存在。

load_dotenv() 似乎没生效

检查 main.py 顶部:

load_dotenv(Path(__file__).resolve().parent.parent / ".env")

这一行必须在 from agent import Agent 之前——因为 agent.py 里读 os.environ["OPENAI_API_KEY"],import 时就执行了,那时候 env 必须已经加载好。

❌ 改了代码但好像没生效

Python 不是 JS——改了 agent.py 之后必须重启 main.py 才能看到效果(Python 的 import 缓存)。

❌ 行为和 ch04 不一样

理论上应该完全一样。如果你改 system prompt 之后AI行为变了,那是正常的(system prompt 影响很大)。把 DEFAULT_PERSONA 改回来。


5. 思考题

题 1:改 persona

DEFAULT_PERSONA 改成下面这段,跑跑看:

DEFAULT_PERSONA = """你是一个广东话茶餐厅老板,用粤语回答所有问题,
但工具调用时一切照常。"""

体验"system prompt 决定 agent 全部人格"这件事。

题 2:动态工具集

Agent.__init__ 加个参数 enabled_tools: list[str] | None = None

def __init__(self, enabled_tools=None):
    # ...
    if enabled_tools is None:
        self.tools = TOOLS_SCHEMA
    else:
        self.tools = [t for t in TOOLS_SCHEMA if t["function"]["name"] in enabled_tools]
    self.messages = [{"role": "system", "content": build_system_prompt(self.tools)}]

然后 Agent(enabled_tools=["read_file", "list_dir"]) 启动一个只读版的 agent。这是 ch08 子 agent "受限工具集" 的预演。

题 3:加项目上下文文件

build_system_prompt 里读一下当前目录的 README.md(如果存在),追加到 prompt 末尾:

readme = Path("README.md")
context = ""
if readme.exists():
    text = readme.read_text(encoding="utf-8")[:2000]
    context = f"\n# 项目上下文(README 摘要)\n{text}\n"

这就是 pi-main 的"项目上下文文件"功能。让你的 agent 一上来就懂这是个什么项目。


这一章你学会了什么

  • ✅ 模块化:tools / agent / main 各司其职
  • class Agent 把状态和方法收拢
  • build_system_prompt() 动态构建 system 消息
  • ✅ system prompt 里塞工具清单 / cwd / 日期是工业级标配

下一章 ch06:把 ch03 那段 inline 的 input("y/N") 抽成正式的钩子(hook),让"加新功能不用改 Agent 代码"。

本章完成后,继续装下一块能力