系统提示词是每次对话的起点,由两部分组成并动态构建,不持久化到 JSONL 文件中:
- 角色定义:从
roles/<name>.md读取,纯文本,用户可直接编辑 - 技能提示词:从
skills/*.md读取,仅enabled不为false的技能会被嵌入
- 程序启动时构建一次
/new创建新会话时重新构建(如果改了 skills 文件,新会话会生效)- 加载旧会话时从 JSONL 恢复消息,然后注入当前最新的系统提示词
你是一个 AI 助手。你可以使用工具来读取文件、写入文件、列出目录内容。请用中文回复。new # 使用 roles/default.md
new D:/project # 指定工作目录
new D:/project code-assistant # 使用 roles/code-assistant.md
new D:/project code-assistant data/my_chats # 指定工作目录 + 数据目录技能是可选的领域能力扩展。一个 skill 定义 AI 在特定领域的行为方式和知识。
skills/<name>.md,使用 YAML frontmatter + Markdown 正文:
---
name: code-reviewer
description: 专业的代码审查助手,能够分析代码质量并提供改进建议
enabled: true
---
你是一个代码审查专家。在审查代码时,请遵循以下原则:
1. 关注代码的可读性和可维护性
2. 指出潜在的 bug 和性能问题
3. 建议符合语言最佳实践的改进方案| 字段 | 必填 | 默认值 | 说明 |
|---|---|---|---|
name |
✅ | - | 技能名称,唯一标识 |
description |
❌ | "" | 简短描述 |
enabled |
❌ | true | 设为 false 则禁用,不嵌入系统提示词 |
- 添加技能:在
skills/目录下新建.md文件,重启生效 - 禁用技能:在 frontmatter 中设
enabled: false,重启或/new生效 - 删除技能:删除文件,重启生效
/skills → 列出所有技能,显示启用/禁用状态
skills/
├── code-reviewer.md
├── data-analyst.md
└── translator.md
src/skill/
├── __init__.py
└── skill_registry.py ← SkillRegistry: 扫描目录、一次性加载、导出已启用提示词
工具是 AI 可以调用的函数,通过 OpenAI API 的 tools 参数声明。模型看到工具定义后自动决定是否调用、如何调用。
src/tools/
├── __init__.py
├── registry.py ← ToolRegistry: register / execute / get_openai_tools
└── builtin/
├── __init__.py ← 汇总导出
├── read_file.py ← 读取文件(支持指定行范围)
├── write_file.py ← 写入文件
└── list_files.py ← 列出目录(支持递归,大目录自动摘要)
| 工具 | 参数 | 说明 |
|---|---|---|
read_file |
filepath, offset, limit |
读取文件,大文件自动分段,末尾附导航 |
write_file |
filepath, content |
写入文件,自动创建父目录 |
list_files |
dirpath, recursive |
列出目录,递归时先输出结构摘要再列文件 |
- 工具对自己领域的数据负责:知道什么信息有用、怎么分段、怎么引导模型下一步
- Registry 不做智能截断,只做纯路由
- 所有文件操作限制在启动时指定的工作目录内,防止路径穿越
- 在
src/tools/builtin/下新建文件,实现register(registry)函数 - 在
builtin/__init__.py中导出 - 在
agent/agent_loop.py的_register_builtin_tools()中调用注册
示例:
# src/tools/builtin/my_tool.py
def register(registry):
def _my_tool(param1: str) -> str:
return f"结果: {param1}"
registry.register(
name="my_tool",
description="我的自定义工具",
parameters={
"type": "object",
"properties": {
"param1": {"type": "string", "description": "参数1"},
},
"required": ["param1"],
},
func=_my_tool,
)所有文件操作工具通过 _safe_path(work_dir, filepath) 校验路径:
os.path.join(work_dir, filepath)拼接os.path.realpath()解析../和符号链接- 检查结果是否在工作目录范围内
- 不在范围内则拒绝
main() / cli/__init__.py
│
├─ build_system_prompt(role_name, skill_registry)
│ ├─ load_role_text("default") → roles/default.md
│ └─ skill_registry.get_active_prompts() → skills/*.md (enabled=true)
│
├─ ChatCLI → AgentLoop → ChatSession (system_prompt)
│ │
│ ├─ AgentLoop._register_builtin_tools()
│ │ └─ ToolRegistry.register(read_file, write_file, list_files)
│ │
│ └─ AgentLoop.run(user_input)
│ ├─ chat_session.send_user_message(content)
│ ├─ tools = tool_registry.get_openai_tools()
│ ├─ chat_session.generate_response(tools=tools)
│ │ └─ client.generate_text(conv, tools=tools)
│ │ └─ openai.chat.completions.create(tools=tools)
│ │ ← tool_calls: [{name, arguments}]
│ ├─ [NEED_TOOL] tool_registry.execute(name, **args)
│ │ └─ send_tool_message(result, call_id)
│ └─ generate_response(tools=tools) ← 循环直到 COMPLETE
│
└─ 持久化 (JSONL)
├─ save(): 跳过 system 消息,只写 header + user/assistant/tool
└─ load(): 从文件读取 → 注入当前 system_prompt
data/conversations/
├── abc123.jsonl ← 一个会话一个文件
└── def456.jsonl
JSONL 格式:
{"type":"header","root_id":"...","metadata":{}}
{"type":"message","msg_id":"...","role":"user","content":"...",...}
{"type":"message","msg_id":"...","role":"assistant","content":"...",...}
- 系统提示词不持久化,每次加载时动态注入
- 会话间隔离,
/list列出所有,/switch <id>切换,/new新建 /tree显示当前会话的消息树,分叉时画分支线
| 命令 | 说明 |
|---|---|
/exit |
退出 |
/help |
帮助 |
/tree |
显示消息树 |
/list |
列出所有会话 |
/switch <id> |
切换会话 |
/new |
创建新会话 |
/skills |
列出技能及启用状态 |
消息是对话的最小单位,通过 parent_id + children_ids 形成树结构。
class Message(BaseModel):
id: str # 8 位 UUID,唯一标识
role: Literal["user", "system", "assistant", "tool"]
parent_id: str # 父消息 id,根消息为 None
content: str # 消息正文
timestamp: datetime # UTC 时间戳,实例化时自动生成
children_ids: list # 子消息 id 列表,形成树
tool_call_id: str # role="tool" 时回指 assistant 的工具调用
tool_calls: list # role="assistant" 时记录工具调用信息四种角色:
| role | 含义 | 特殊字段 |
|---|---|---|
system |
系统提示词,不持久化 | - |
user |
用户输入 | - |
assistant |
AI 回复(文本或工具调用) | tool_calls |
tool |
工具执行结果 | tool_call_id |
一棵消息树。所有消息都存在 messages 字典里(key=id),通过 parent_id 和 children_ids 链接。
class Conversation(BaseModel):
id: str # 8 位 UUID
messages: Dict[str, Message] # 消息字典
root_id: str # 根消息(系统提示词)id
current_message_id: str # 光标位置,新消息默认挂在这下面
metadata: dict # 额外元数据核心方法:
| 方法 | 说明 |
|---|---|
add_message(msg) |
添加消息,维护父子关系,更新 current_message_id |
change_current_message(id) |
移动光标,实现分支切换 |
get_message_chain(id) |
从指定消息沿 parent_id 走到根,返回有序列表 |
get_openai_messages(id) |
链 → OpenAI API 格式(含 tool_calls) |
get_children_messages(id) |
获取直接子节点 |
get_branch(id) |
获取整棵子树 |
get_leaf_ids() |
获取所有叶子节点 |
is_leaf(id) |
判断是否为叶子 |
对话天然是一棵树。/tree 命令展示当前会话的消息树,每条消息前面带 [N] 序号:
会话 [abc123]:
[0] 系统提示词: 你是一个助手
[1] 你: 你好
[2] AI: 你好!有什么可以帮你的?
[3] 你: 北京天气怎么样
[4] AI: [工具调用: get_weather]
[5] 工具结果: {"temp":25}
[6] AI: 北京今天晴,25度
正常对话沿一条路径走,current_message_id 始终指向最新消息。要创建分支:
/tree查看序号/<数字>切换到指定节点,例如/2回到 AI 第一次回复的位置- 发消息,新消息从该节点分叉
你: /2
已切换到 [2] AI: 你好!有什么可以帮你的?
你: 不对,重新回答
AI: 嗨,你好呀!
你: /tree
会话 [abc123]:
[0] 系统提示词: 你是一个助手
[1] 你: 你好
[2] AI: 你好!有什么可以帮你的?
├─ [3] 你: 北京天气怎么样 ← 旧分支
│ ├─ [4] AI: [工具调用: get_weather]
│ ├─ [5] 工具结果: {"temp":25}
│ └─ [6] AI: 北京今天晴,25度
└─ [3] 你: 不对,重新回答 ← 新分支
└─ [4] AI: 嗨,你好呀!
序号是深度优先遍历分配的,可以切换到任意节点(不管是用户消息、AI 回复还是工具调用结果)。切换后发消息,对话就从该节点分叉,互不影响。
对话会话管理器,封装了 LLM 客户端、持久化和多会话切换。
class ChatSession:
llm: Client # LLM 客户端
repository: ConversationRepository # 持久化层
system_prompt: str # 系统提示词
all_conversations: Dict[str, Conversation] # 全部会话
_current_conversation: Conversation # 当前活跃会话核心方法:
| 方法 | 说明 |
|---|---|
current_conversation (property) |
懒加载当前会话,首次访问时创建 |
send_user_message(content) |
创建 user 消息并持久化 |
send_tool_message(content, tool_call_id) |
创建 tool 消息并持久化 |
generate_response(tools) |
调用 LLM,处理 tool_calls,返回 SessionResponse |
switch_conversation(id) |
切换会话 |
create_new_conversation() |
新建会话 |
list_conversations() |
列出所有会话 |
generate_response() 的返回值,告诉外部调用方下一步该做什么:
@dataclass
class SessionResponse:
content: str # 显示文本
status: SessionStatus # COMPLETE / ERROR / NEED_TOOL
should_exit: bool # COMPLETE 时为 True
tool_calls: list # NEED_TOOL 时携带工具调用信息
error: str # ERROR 时携带错误信息三种状态:
| 状态 | 含义 | 调用方动作 |
|---|---|---|
COMPLETE |
对话完成 | 展示 content,等下一轮用户输入 |
ERROR |
出错 | 展示 error |
NEED_TOOL |
需要执行工具 | 执行 tool_calls,调 send_tool_message,再次 generate_response |
外层循环,封装了完整的"用户输入 → LLM → 工具 → LLM → ... → 回复"流程:
class AgentLoop:
chat_session: ChatSession
tool_registry: ToolRegistry
run(content: str) -> str # 单轮:一条用户输入 → 完整工具链 → 最终回复run() 内部是一个 while 循环,自动处理链式工具调用(LLM 调工具 → 执行 → LLM 收到结果再调工具 → ...),直到 COMPLETE 或 ERROR 或超过最大轮次(30 轮)。
一个会话一个 .jsonl 文件,存储在 data/conversations/ 下。
data/conversations/
├── abc123.jsonl
└── def456.jsonl
JSONL 格式:
{"type":"header","root_id":"009f13ac","metadata":{}}
{"type":"message","msg_id":"82981a45","role":"user","content":"你好","parent_id":"009f13ac","timestamp":"2026-07-24T12:35:55Z","tool_call_id":null,"tool_calls":null}
{"type":"message","msg_id":"b06c61f6","role":"assistant","content":"你好!","parent_id":"82981a45","timestamp":"2026-07-24T12:35:59Z","tool_call_id":null,"tool_calls":null}设计要点:
- 第一行是 header(
root_id+metadata),之后每行是一条消息 - 系统提示词(role=system)不持久化,保存时跳过
- 加载时从 header 获取
root_id,用当前system_prompt新建系统消息注入 children_ids不存,加载时从parent_id全部重建- 追加写,不修改已有内容
- 一个文件一个会话,删除/备份简单
加载流程:
启动 → repository.list_all_conversations()
→ 逐个 load(id, system_prompt)
→ 读 header → 读消息行 → 建系统消息 → 重建 children_ids
→ 注入 all_conversations
/new → Conversation(prompt_message)
→ 首次 send_user_message 时写 header + user 消息
/switch → all_conversations[conv_id] 设为 current
# 启动(自动加载历史会话)
new
# 指定工作目录
new D:/my_project
# 完整参数:工作目录 + 角色 + 数据目录
new D:/my_project code-assistant data/my_chats你: 你好
AI: 你好!有什么可以帮你的?
你: /list ← 列出所有会话
共 1 个会话:
[abc12345] 8 条消息 ← 当前
你: /new ← 创建新会话
已创建新会话 [def67890]
你: /switch abc12345 ← 切回旧会话
已切换到会话 [abc12345]
你: /tree ← 查看消息树(带序号)
会话 [abc12345]:
[0] 系统提示词: 你是一个助手
[1] 你: 你好
[2] AI: 你好!有什么可以帮你的?
[3] 你: 今天天气怎么样
[4] AI: [工具调用: get_weather]
[5] 工具结果: {"temp":25}
[6] AI: 今天 25 度,天气不错
你: /2 ← 回到序号 [2] 的节点
已切换到 [2] AI: 你好!有什么可以帮你的?
你: 给我另一个回答 ← 从 [2] 分叉,创建新分支
AI: 嗨,你好呀!
你: /tree
[0] 系统提示词: ...
[1] 你: 你好
[2] AI: 你好!有什么可以帮你的?
├─ [3] 你: 今天天气怎么样 ← 旧分支
│ └─ ...
└─ [3] 你: 给我另一个回答 ← 新分支
└─ [4] AI: 嗨,你好呀!
你: /exit ← 退出
再见!