Skip to content

About

一个自研编码agent项目

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

21 Commits

Folders and files

Repository files navigation

框架设计文档

系统提示词 (System Prompt)

系统提示词是每次对话的起点,由两部分组成并动态构建,不持久化到 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)

技能是可选的领域能力扩展。一个 skill 定义 AI 在特定领域的行为方式和知识。

文件格式

skills/<name>.md,使用 YAML frontmatter + Markdown 正文:

---
name: code-reviewer
description: 专业的代码审查助手,能够分析代码质量并提供改进建议
enabled: true
---

你是一个代码审查专家。在审查代码时,请遵循以下原则:
1. 关注代码的可读性和可维护性
2. 指出潜在的 bug 和性能问题
3. 建议符合语言最佳实践的改进方案

frontmatter 字段

字段 必填 默认值 说明
name ✅ - 技能名称,唯一标识
description ❌ "" 简短描述
enabled ❌ true 设为 false 则禁用,不嵌入系统提示词

管理方式

  • 添加技能:在 skills/ 目录下新建 .md 文件,重启生效
  • 禁用技能:在 frontmatter 中设 enabled: false,重启或 /new 生效
  • 删除技能:删除文件,重启生效

CLI 查看

/skills     → 列出所有技能,显示启用/禁用状态

代码结构

skills/
├── code-reviewer.md
├── data-analyst.md
└── translator.md

src/skill/
├── __init__.py
└── skill_registry.py    ← SkillRegistry: 扫描目录、一次性加载、导出已启用提示词

工具 (Tool)

工具是 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 不做智能截断,只做纯路由
  • 所有文件操作限制在启动时指定的工作目录内,防止路径穿越

添加新工具

  1. 在 src/tools/builtin/ 下新建文件,实现 register(registry) 函数
  2. 在 builtin/__init__.py 中导出
  3. 在 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) 校验路径:

  1. os.path.join(work_dir, filepath) 拼接
  2. os.path.realpath() 解析 ../ 和符号链接
  3. 检查结果是否在工作目录范围内
  4. 不在范围内则拒绝

调用链路

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 显示当前会话的消息树,分叉时画分支线

CLI 命令

命令 说明
/exit 退出
/help 帮助
/tree 显示消息树
/list 列出所有会话
/switch <id> 切换会话
/new 创建新会话
/skills 列出技能及启用状态

会话系统 (Session)

数据模型

Message

消息是对话的最小单位,通过 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

Conversation

一棵消息树。所有消息都存在 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 始终指向最新消息。要创建分支:

  1. /tree 查看序号
  2. /<数字> 切换到指定节点,例如 /2 回到 AI 第一次回复的位置
  3. 发消息,新消息从该节点分叉
你: /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 回复还是工具调用结果)。切换后发消息,对话就从该节点分叉,互不影响。

ChatSession

对话会话管理器,封装了 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() 列出所有会话

SessionResponse

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

AgentLoop

外层循环,封装了完整的"用户输入 → 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                    ← 退出
再见!

About

一个自研编码agent项目

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages