最近在整理 Bot 类项目时发现一个很现实的问题:每接一个新的对话机器人需求,都要重新搭一遍工程骨架、配置一遍平台接入、重新写一份 Prompt 模板。项目之间明明高度相似,却因为缺乏统一的模板机制,导致重复劳动和配置不一致。Dr Eggbot v0.1.0 正是围绕这个问题设计的模板管理工具,它把“可复用的 Bot 工程模板”作为核心单元,支持模板创建、校验、渲染、打包和分享。本文会带你完整拆解这个版本的模板模型,并给出一套可运行的最小实现,帮助你在自己的 Bot 项目里落地模板化开发。
文章适合正在做聊天机器人、自动化 Bot、项目脚手架工具的同学,也适合对“模板引擎 + 配置化生成”感兴趣的后端开发者。读完你会掌握 Bot 模板的目录设计、manifest 约定、变量渲染方式,以及模板分享和导入的完整流程。
1. Dr Eggbot 是什么:为什么 Bot 开发需要模板
1.1 Bot 项目中的重复工作
一个典型的 Bot 工程,无论对接什么平台,通常都包含以下部分:
- 机器人启动入口,负责拉取平台配置并监听事件。
- 平台接入层,比如 Webhook 回调、WebSocket 长连接、消息格式转换。
- 对话处理逻辑,包括意图识别、上下文管理、多轮对话状态机。
- 系统提示词模板,也就是 Bot 的“人设”和“行为准则”。
- 部署配置,包括环境变量、启动脚本、日志目录等。
这些内容在不同 Bot 项目之间高度相似。如果每次都从空白目录开始手写,很容易出现两种问题:一是新项目初始化慢,几个小时内都在做重复配置;二是不同项目的目录结构和命名习惯逐渐分叉,团队成员切换项目时需要重新熟悉。
模板化开发的核心思路是:把 Bot 工程中最稳定的结构抽取为模板,把需要变化的部分定义为变量。创建新项目时,只需要提供少量变量值,就能生成一个完整可运行的工程目录。
1.2 Dr Eggbot v0.1.0 的定位
Dr Eggbot v0.1.0 是一个面向 Bot 工程场景的模板管理工具。它的核心能力可以概括为四点:
- 模板创建:通过一个简单的 init 命令,生成模板目录和 manifest 清单。
- 模板校验:检查 manifest 字段是否完整、变量是否有默认值。
- 模板渲染:把模板文件中的
{{变量}}占位符替换成实际值,输出新工程。 - 模板分享:将整个模板目录打包成归档文件,别人拉取后直接生成 Bot 项目。
v0.1.0 属于早期版本,重点是先把“定义模板、使用模板、分享模板”这条主链路跑通。它不试图取代成熟的脚手架工具,而是专注在 Bot 模板这一细分场景。
1.3 和常见脚手架工具的区别
很多脚手架工具也能生成项目,但 Dr Eggbot 更强调“模板本身就是可分享的资产”。
| 维度 | 传统脚手架工具 | Dr Eggbot v0.1.0 模板机制 |
|---|---|---|
| 模板来源 | 通常由工具内置,用户较少自定义 | 模板由开发者创建,目录即模板 |
| 分享方式 | 更新工具才能更新模板 | 打包 zip 后可直接分发 |
| 变量体系 | 脚手架交互式提问后生成 | manifest 声明变量,渲染时统一替换 |
| 适用范围 | 通用项目骨架 | 面向 Bot 工程的目录与配置结构 |
这种设计让模板能够跟随团队内部的最佳实践持续演进,而不是被冻结在工具版本里。
2. 环境准备与最小可运行示例
2.1 环境版本说明
本文示例使用 Python 3.10+,只需要标准库即可运行,不依赖第三方包。操作系统方面,Windows、macOS、Linux 均适用,命令部分会同时给出 bash 和 Windows 命令行写法。
Python >= 3.10如果你本机还没有 Python,建议先安装官方版本,并确认命令行里能执行:
python --versionWindows 下可能是py --version,后续命令可相应替换。
2.2 创建项目目录
先创建一个演示目录,后续所有代码都放在这个目录下:
mkdir eggbot-demo cd eggbot-demo目录内部结构如下:
eggbot-demo/ ├── eggbot.py # Dr Eggbot 模板管理主脚本 ├── templates/ # 模板存放目录 ├── output/ # 渲染生成的项目目录 ├── dist/ # 模板打包输出目录 └── .eggbot_cache/ # 拉取模板时的临时缓存templates目录是模板的根目录,每个子目录代表一个模板;output目录用于存放渲染结果;dist目录存放打包后的 zip 文件。
2.3 eggbot.py 脚本结构
我们用一个单文件 Python 脚本实现 v0.1.0 的核心命令。脚本支持五个子命令:
init:创建一个新模板骨架。validate:校验模板 manifest 和目录结构。render:根据模板和变量渲染出 Bot 工程。pack:将模板打包成可分享的 zip 文件。pull:从 zip 归档中导入模板并生成项目。
把这些命令集中在一个脚本里,便于你理解整体流程,也方便后续扩展。
3. Bot 模板的核心组成
3.1 manifest.json:模板的“说明书”
Dr Eggbot 约定,每个模板目录下必须有一个manifest.json。这个文件描述模板的基本信息、版本、变量列表和渲染规则。
一个最小可用的 manifest 如下:
{ "schema_version": "1.0", "id": "demo-bot", "name": "Demo Bot", "version": "0.1.0", "description": "一个用于演示 Dr Eggbot 模板机制的最小 Bot 工程", "author": "your-team", "variables": [ { "name": "bot_name", "default": "demo-bot", "description": "Bot 实例名称,用于日志与标识" }, { "name": "platform", "default": "console", "description": "对接平台,可选 console / wechat / discord" }, { "name": "system_prompt", "default": "你是一个乐于助人的助手", "description": "Bot 的系统提示词模板" } ], "render": { "include": ["project", "assets"], "exclude": ["__pycache__", "*.pyc", ".git"], "target": "output" } }字段含义如下:
schema_version:manifest 格式版本。后续升级时通过这个字段做兼容判断。id:模板唯一标识,推荐使用短横线命名,例如demo-bot。name:模板展示名称,可以包含空格和中文。version:模板版本号,建议遵循语义化版本。variables:模板变量列表,渲染时需要替换的占位符都在这里声明。render.include:哪些子目录属于模板内容,会被复制和渲染。render.exclude:复制过程中需要排除的文件或目录。
3.2 模板变量
模板变量是“模板与实例”之间的桥梁。创建模板时,你只需要在文件中写入{{变量名}}形式的占位符;渲染时,Dr Eggbot 会根据 manifest 中声明的变量列表,将这些占位符替换为具体值。
例如在系统提示词模板中:
你是 {{ bot_name }},运行在 {{ platform }} 平台。 {{ system_prompt }}渲染后可能变成:
你是 my-first-bot,运行在 console 平台。 你是一个乐于助人的助手。每个变量都应该有默认值,这样使用模板的人即使不输入任何参数,也能渲染出可运行的工程。变量名的命名建议统一使用小写字母和下划线。
3.3 project 目录与 assets 目录
一个模板可以包含多个子目录。在 v0.1.0 中,建议至少区分两类内容:
project:直接生成到 Bot 工程中的代码文件。assets:非代码类资源,比如 Prompt 模板、菜单配置、说明文档。
之所以做区分,是因为代码文件和提示词资源的更新频率不同。Bot 的项目代码可能需要随功能迭代变化,而提示词模板往往由运营或算法同学维护,拆开存放更清晰。
3.4 模板与实例的映射关系
把一次模板渲染理解为“由模板创建实例”的过程:
模板 + 变量值 -> Bot 工程实例模板目录中project下每个文件,都会在输出目录中生成一个对应文件。例如模板中存在project/bot.py,渲染后会生成output/my-bot/bot.py。这个映射是逐文件复制并替换变量,而不是在内存里拼接字符串,因此目录层级能够完整保留。
4. 实战一:创建并渲染你的第一个 Bot 模板
4.1 命令入口与 init 操作
先把完整脚本保存为eggbot.py。脚本较长,但每一段都对应一个明确职责。
#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ eggbot.py - Dr Eggbot v0.1.0 模板管理示例 支持命令: init / validate / render / pack / pull 运行方式: python eggbot.py <command> [options] """ import argparse import fnmatch import json import re import shutil import sys import zipfile from pathlib import Path TEMPLATE_DIR = Path("templates") OUTPUT_DIR = Path("output") DIST_DIR = Path("dist") CACHE_DIR = Path(".eggbot_cache") EXCLUDE_PATTERNS = ["__pycache__", "*.pyc", ".git", ".DS_Store", ".gitkeep"] REQUIRED_MANIFEST_FIELDS = [ "schema_version", "id", "name", "version", "variables", ] def is_excluded(path: Path, root: Path, patterns): rel = path.relative_to(root).as_posix() for pattern in patterns: if fnmatch.fnmatch(rel, pattern): return True if fnmatch.fnmatch(path.name, pattern): return True return False def load_manifest(tpl_dir: Path) -> dict: manifest_path = tpl_dir / "manifest.json" if not manifest_path.exists(): raise SystemExit(f"缺少 manifest.json: {manifest_path}") try: manifest = json.loads(manifest_path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise SystemExit(f"manifest.json 解析失败: {exc}") return manifest def render_string(text: str, values: dict) -> str: pattern = re.compile(r"\{\{\s*([\w.]+)\s*\}\}") def repl(match): key = match.group(1).strip() if key in values and values[key] is not None: return str(values[key]) return match.group(0) return pattern.sub(repl, text) def render_template(tpl_dir: Path, values: dict, output_dir: Path): manifest = load_manifest(tpl_dir) include_dirs = manifest.get("render", {}).get("include", ["project", "assets"]) exclude_patterns = manifest.get("render", {}).get("exclude", EXCLUDE_PATTERNS) for sub_dir in include_dirs: src_dir = tpl_dir / sub_dir if not src_dir.exists(): continue for file in sorted(src_dir.rglob("*")): if not file.is_file(): continue if is_excluded(file, src_dir, exclude_patterns): continue rel = file.relative_to(src_dir) out_file = output_dir / rel out_file.parent.mkdir(parents=True, exist_ok=True) content = file.read_text(encoding="utf-8") content = render_string(content, values) out_file.write_text(content, encoding="utf-8")init命令负责创建模板骨架。它会自动生成 manifest 和几个示例文件,方便你在此基础上修改。
def cmd_init(args): raw_name = args.name safe_name = re.sub(r"[^A-Za-z0-9_-]+", "-", raw_name.strip().lower()).strip("-") if not safe_name: raise SystemExit("模板名称非法,只能包含字母、数字、下划线和中划线") tpl_dir = TEMPLATE_DIR / safe_name if tpl_dir.exists(): raise SystemExit(f"模板已存在: {tpl_dir}") (tpl_dir / "project").mkdir(parents=True) (tpl_dir / "assets").mkdir(parents=True) manifest = { "schema_version": "1.0", "id": safe_name, "name": raw_name, "version": "0.1.0", "description": args.desc or "", "author": args.author or "", "variables": [ { "name": "bot_name", "default": safe_name, "description": "Bot 实例名称,用于日志与标识" }, { "name": "platform", "default": "console", "description": "对接平台,可选 console / wechat / discord" }, { "name": "system_prompt", "default": "你是一个乐于助人的助手", "description": "Bot 的系统提示词模板" } ], "render": { "include": ["project", "assets"], "exclude": ["__pycache__", "*.pyc", ".git", ".gitkeep"], "target": "output" } } (tpl_dir / "manifest.json").write_text( json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8" ) bot_py = '''import os def main(): bot_name = "{{ bot_name }}" platform = "{{ platform }}" print(f"启动 {bot_name} 实例,对接平台:{platform}") if __name__ == "__main__": main() ''' (tpl_dir / "project" / "bot.py").write_text(bot_py, encoding="utf-8") readme = '''# {{ bot_name }} 这是一个由 Dr Eggbot v0.1.0 生成的 Bot 工程。 - 平台:{{ platform }} - 系统提示词:{{ system_prompt }} ''' (tpl_dir / "project" / "README.md").write_text(readme, encoding="utf-8") prompt = '''你是 {{ bot_name }},运行在 {{ platform }} 平台。 {{ system_prompt }} ''' (tpl_dir / "assets" / "system_prompt.md").write_text(prompt, encoding="utf-8") print(f"模板创建完成: {tpl_dir}")4.2 初始化示例模板
执行 init 命令,创建一个名为demo-bot的模板:
python eggbot.py init demo-bot --desc "一个用于演示的模板" --author "your-team"命令行支持--desc和--author可选参数。执行完后,templates/demo-bot目录下会自动生成以下文件:
templates/demo-bot/ ├── manifest.json ├── project/ │ ├── bot.py │ └── README.md └── assets/ └── system_prompt.md现在打开templates/demo-bot/project/bot.py,你会看到模板文件里的占位符:
import os def main(): bot_name = "{{ bot_name }}" platform = "{{ platform }}" print(f"启动 {bot_name} 实例,对接平台:{platform}") if __name__ == "__main__": main()注意,这里的{{ bot_name }}并不是 Python 语法,而是模板占位符。渲染完成后,它会被替换成实际的 Bot 名称。
4.3 校验模板
模板创建好后,建议先做一次校验,确认 manifest 结构完整。校验逻辑并不复杂:检查必填字段是否存在、变量是否有默认值。
def cmd_validate(args): tpl_dir = TEMPLATE_DIR / args.name if not tpl_dir.exists(): raise SystemExit(f"模板不存在: {tpl_dir}") manifest = load_manifest(tpl_dir) missing = [field for field in REQUIRED_MANIFEST_FIELDS if field not in manifest] if missing: raise SystemExit(f"manifest.json 缺少字段: {missing}") variables = manifest.get("variables", []) if not isinstance(variables, list): raise SystemExit("variables 必须是数组") for var in variables: if "name" not in var: raise SystemExit("variables 中存在缺少 name 的条目") if "default" not in var: raise SystemExit(f"变量 {var.get('name')} 缺少 default 默认值") print(f"模板校验通过: {tpl_dir}")运行:
python eggbot.py validate demo-bot如果输出模板校验通过,说明模板可以进入渲染环节。
4.4 渲染模板生成 Bot 工程
渲染命令读取 manifest 中 variables 的默认值,同时允许通过--var覆盖局部变量。
def cmd_render(args): tpl_dir = TEMPLATE_DIR / args.name if not tpl_dir.exists(): raise SystemExit(f"模板不存在: {tpl_dir}") manifest = load_manifest(tpl_dir) values = {} for var in manifest.get("variables", []): values[var["name"]] = var.get("default", "") for item in args.var or []: if "=" not in item: raise SystemExit(f"参数格式错误: {item},期望 key=value") key, value = item.split("=", 1) values[key.strip()] = value output_dir = OUTPUT_DIR / values.get("bot_name", manifest["id"]) if output_dir.exists(): shutil.rmtree(output_dir) render_template(tpl_dir, values, output_dir) print(f"渲染完成: {output_dir}")执行渲染:
python eggbot.py render demo-bot --var bot_name=my-first-bot --var platform=console预期的输出:
渲染完成: output/my-first-bot查看生成的内容:
find output/my-first-bot -type f你会看到bot.py、README.md、system_prompt.md三个文件都已生成。再打开output/my-first-bot/bot.py:
import os def main(): bot_name = "my-first-bot" platform = "console" print(f"启动 {bot_name} 实例,对接平台:{platform}") if __name__ == "__main__": main()占位符已经被替换成了实际值。这说明“模板 + 变量”已经成功转化成了一个独立的 Bot 工程实例。
5. 实战二:打包、分享与导入模板
5.1 pack 打包
模板的分享依赖于打包能力。pack命令会把templates/demo-bot目录压缩成 zip 文件,压缩包内以模板 id 作为根目录。
def cmd_pack(args): tpl_dir = TEMPLATE_DIR / args.name if not tpl_dir.exists(): raise SystemExit(f"模板不存在: {tpl_dir}") manifest = load_manifest(tpl_dir) DIST_DIR.mkdir(exist_ok=True) zip_name = f"{manifest['id']}-{manifest['version']}.zip" zip_path = DIST_DIR / zip_name with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: for file in sorted(tpl_dir.rglob("*")): if not file.is_file(): continue if is_excluded(file, tpl_dir, EXCLUDE_PATTERNS): continue arcname = f"{manifest['id']}/{file.relative_to(tpl_dir).as_posix()}" zf.write(file, arcname) print(f"打包完成: {zip_path}")执行:
python eggbot.py pack demo-bot输出:
打包完成: dist/demo-bot-0.1.0.zip此时,dist目录下的demo-bot-0.1.0.zip就是可以分享给同事或团队内其他项目的模板包。
5.2 模板分享的几种方式
拿到 zip 包后,分享方式很灵活:
- 直接通过企业网盘、内部存储分发,适合小团队。
- 上传到 Git 仓库的
templates目录,配合版本标签管理。 - 存入 Nexus 或 Artifactory 等制品仓库,按版本拉取。
- 统一放到对象存储,再由模板中心服务下发。
对于 v0.1.0 阶段,最简单的做法是把 zip 文件放到一个只有团队可见的目录或代码仓库中,保持“一个版本一个 zip”的命名约定。
5.3 pull 导入模板并生成项目
接收方拿到 zip 后,使用pull命令导入并生成项目。pull会先解压到缓存目录,再找到合法的manifest.json,然后执行渲染。
def cmd_pull(args): archive = Path(args.archive) if not archive.exists(): raise SystemExit(f"归档不存在: {archive}") cache_dir = CACHE_DIR / archive.stem if cache_dir.exists(): shutil.rmtree(cache_dir) with zipfile.ZipFile(archive) as zf: zf.extractall(cache_dir) manifest = None tpl_root = cache_dir for candidate in cache_dir.rglob("manifest.json"): try: m = json.loads(candidate.read_text(encoding="utf-8")) except json.JSONDecodeError: continue if m.get("schema_version") and m.get("id"): manifest = m tpl_root = candidate.parent break if not manifest: raise SystemExit("未在归档中找到合法 manifest.json") values = {} for var in manifest.get("variables", []): values[var["name"]] = var.get("default", "") for item in args.var or []: if "=" not in item: raise SystemExit(f"参数格式错误: {item},期望 key=value") key, value = item.split("=", 1) values[key.strip()] = value if args.name: values["bot_name"] = args.name output_dir = OUTPUT_DIR / values.get("bot_name", manifest["id"]) if output_dir.exists(): shutil.rmtree(output_dir) render_template(tpl_root, values, output_dir) print(f"模板导入完成,项目已生成: {output_dir}")执行:
python eggbot.py pull dist/demo-bot-0.1.0.zip --name from-remote-bot输出:
模板导入完成,项目已生成: output/from-remote-bot查看生成目录,你会发现和本地渲染效果一致,说明模板已经成功从 zip 包恢复成了完整工程。
5.4 分享模板时的注意事项
分享模板时,有几个问题必须提前检查:
- 模板中不要包含本机绝对路径。
- 不要包含
.env文件或任何密钥信息。 - manifest 的 id 不要随意修改,否则会影响已有引用。
- 打包前确认
exclude已经排除.git目录。
这些点看似基础,却是团队协作里最常踩的坑。模板一旦被多人使用,任何不规范都会成倍放大。
6. 常见错误与排查思路
在使用模板机制的过程中,以下问题出现频率较高:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| manifest.json 解析报 JSONDecodeError | 手写 JSON 时多写逗号或注释 | 用python -m json.tool templates/demo-bot/manifest.json校验 |
渲染后仍有{{ bot_name }}残留 | 变量名大小写不一致,或变量未在 manifest 中声明 | 统一使用小写命名,检查 variables 列表 |
| pull 时提示未找到合法 manifest | zip 包根目录层级混乱,rglob 找不到 | 打包时保持根目录为模板 id |
生成的项目出现__pycache__ | exclude 未包含 pyc 规则 | 在 render.exclude 中加入__pycache__和*.pyc |
| 模板更新后 pull 还是旧内容 | 本地缓存目录没有清理 | 按模板 id + version 建立缓存,发版时升级 version |
| 变量默认值包含中文时乱码 | 文件写入编码不一致 | 统一使用encoding="utf-8"读写模板文件 |
| 打包后 zip 体积过大 | 混入了.git或本地依赖 | 完善 exclude 规则,打包前人工检查目录内容 |
如果遇到列表之外的异常,建议按以下顺序排查:
- 先看模板目录是否完整,
manifest.json是否存在。 - 用 JSON 校验工具检查 manifest 格式。
- 手动打开模板文件,确认占位符写法和变量名。
- 查看渲染输出目录,对比哪个文件不符合预期。
- 如果是分享场景,直接解压 zip 检查包内目录结构。
7. 工程化最佳实践
7.1 模板设计规范
模板看起来自由,但建议从一开始就约定规范:
- 模板 id 使用 kebab-case,例如
customer-service-bot。 - 模板 name 可以用于展示,不参与文件路径。
- 变量名统一小写下划线,例如
bot_name、platform。 - 每个变量必须提供默认值和 description,方便别人理解。
- manifest 中只保留必要字段,不要堆放业务配置。
模板文件的占位符命名最好和真实代码中的变量名保持一致。比如系统提示词模板里写{{ system_prompt }},那么后续程序读取环境变量时也应该叫SYSTEM_PROMPT,减少认知负担。
7.2 敏感信息与安全边界
模板是会被复制的资源,因此必须格外注意敏感信息。
- 不要把 API Key、Token、密码写入 manifest 默认值。
- 不要把
.env文件纳入模板目录。 - 在模板 README 中说明需要注入哪些环境变量。
- 部署时给 Bot 配置最小权限,只授予聊天、读取等必要能力。
举个例子,如果模板里需要配置微信 Bot 的凭证,正确做法是预留WECHAT_TOKEN环境变量占位,而不是在模板里写真实 token。这样可以避免模板在分享过程中泄露敏感信息。
对于需要执行外部操作或涉及权限变更的 Bot,生产使用前必须在测试环境验证,并保留完整操作审计日志。任何自动化 Bot 都不应该具备超出任务范围的系统权限。
7.3 模板版本管理与兼容性
v0.1.0 已经支持version字段,但版本管理不只是改数字。建议配合以下约定:
- 模板的
id一旦发布就不要再改。 - 每次修改模板内容,同步升级 manifest 中的
version。 - 在模板目录中维护
CHANGELOG.md,记录每次变更内容。 - 如果模板生成的项目升级成本较高,在 manifest 中增加
upgrade_notes字段。
当多个团队共享模板时,最好建立“模板版本与项目版本”的对应关系。例如某个项目基于模板 v0.1.0 生成,后续模板升级到 v0.2.0 时,项目可以选择迁移到新模板,也可以继续使用旧版模板。
7.4 用自动化测试保障模板质量
模板属于“生成代码的代码”,质量直接影响下游所有项目。建议对模板做自动化测试:
- 准备一组固定变量值,作为测试 fixture。
- 在 CI 中执行
validate和render。 - 对渲染结果做快照对比,发现异常及时暴露。
- 在测试环境运行渲染出的 Bot,验证启动和基础对话流程。
模板的改动虽然通常不大,但一句{{写错就可能导致所有下游项目渲染失败。自动化测试是投入产出比很高的保障手段。
8. 总结与下一步方向
Dr Eggbot v0.1.0 提供了一套轻量的 Bot 模板管理思路:用模板目录描述工程结构,用 manifest.json 声明元信息和变量,用渲染过程生成项目实例,再用打包分享让模板在团队内流动。这套思路不依赖后端服务或复杂框架,纯 Python 标准库就能实现,非常适合小团队快速落地。
如果你准备在项目中应用这套方案,可以从一个最小模板开始,先把init、render、pack三个命令跑通,再逐步补充校验、缓存和自动化测试。后续可以继续扩展的方向包括:渲染前后钩子、模板嵌套与组合、可视化模板管理界面、模板远程仓库协议等。模板化开发的收益,会随着模板数量增加和使用频率提升而越来越明显。