Agent 开发(四)—— 扩展篇:功能扩展

一、从终端命令到自然语言

1.1 传统 CLI 的痛点

在扩展之前,我们只有一条命令:

bash 复制代码
commit-agent --stage

这个命令只做一件事:生成 commit message 并提交。如果你想做其他操作------切换分支、查看日志、合并代码------你需要记住另一套命令:

bash 复制代码
git switch main
git log --oneline -5
git merge feature-x
git reset --soft HEAD~1
git stash push -m "wip"

这里有几个问题:

  1. 记忆成本高:每个操作有独立的命令、参数、标志,你需要记住它们的具体写法
  2. 组合操作麻烦:想"先 stash 当前工作,再切换到 main 分支,合并 feature,再切回来 pop"------需要 4 条命令串起来
  3. 错误成本高git reset --hard 敲错了就是数据丢失
  4. 上下文割裂:每条命令独立执行,没有"状态",没有"之前发生了什么"

1.2 自然语言交互的优势

把 Git Agent 从"单条命令"升级为"对话式助手"后,上述问题全部消失:

对比维度 传统 CLI 自然语言 Agent
学习成本 需记忆命令和参数 直接说话就行
组合操作 多条命令手动串联 一句话 = 多步操作
错误防护 敲回车即执行,无回滚 LLM 会先检查状态,危险操作需确认
上下文 无状态 Agent 记住对话历史,知道"之前做了什么"
模糊匹配 参数写错就失败 "切到 main" 和 "切换到 main 分支" 都理解

举个例子:

CLI 方式:

bash 复制代码
git stash push -m "temp work"
git checkout main
git pull origin main
git checkout feature
git stash pop

Agent 方式:

复制代码
> 帮我暂存当前工作,切到 main 拉取最新,再切回来恢复

Agent 自动决定调用的工具序列:stash_push → switch_branch → ...

1.3 交互模式的演进

复制代码
v1(实战篇)            v2(扩展篇)
单向流程                对话式 REPL

CLI 参数 → 执行        你:当前在哪个分支?
                        Agent:当前在 main 分支
                       你:切到 feature-x
                        Agent:已切换到 feature-x
                       你:审查代码变更
                        Agent:调 get_working_diff → 给你分析结果

v1 是"问一次答一次",v2 是"持续对话,Agent 自主决策"。


二、架构演进:从单向流程到 Agent Loop

2.1 架构变化

复制代码
v1(实战篇)                          v2(扩展篇)

cli.py → agent.py                     cli.py → agent.py → tools.py
         → llm_client.py                       → llm_client.py (对话历史版)
         → git_utils.py                         → git_utils.py (17 个函数)
         → prompts.py                           → tools.py (NEW: 工具定义 + 调度)
                                                 → prompts.py (多工具提示词)

数据流:
用户输入 → LLM → 文本 → CLI 输出       用户输入 → LLM → 工具调用 → 执行 → 结果回 LLM → 最终回复
                                        ↑_____________________________________________↓
                                                      Agent Loop

核心变化:引入 tools.py 作为工具注册中心,将工具定义和调度逻辑从 llm_client.pyagent.py 中分离出来。

2.2 Agent Loop:思考→行动→观察

Agent Loop 是这个架构的灵魂。每次用户输入,Agent 进入一个循环:

复制代码
用户输入
  │
  ▼
┌─────────────────────────────┐
│  LLM 决策                    │
│  (调用工具 or 回复用户)        │
└──────────┬──────────────────┘
           │
      ┌────┴────┐
      │         │
  调工具      回复文本
      │         │
      ▼         ▼
  执行函数    显示给用户
      │         │
      ▼         │
  结果回传──────┘
  (继续循环)

关键实现(agent.py):

python 复制代码
def run_agent_turn(client, user_input):
    client.add_message("user", user_input)
    tools = get_tool_definitions()

    for _ in range(MAX_TURNS):        # MAX_TURNS = 10 防止无限循环
        response = client.send_with_tools(tools)
        msg = response.choices[0].message

        if msg.tool_calls:
            # 执行每个工具调用
            for tool_call in msg.tool_calls:
                result = execute_tool(tool_call.function.name, args)
                client.add_tool_result(tool_call.id, result)
            # → 继续循环,LLM 基于工具结果再次决策
        else:
            # LLM 返回文本,本轮结束
            return msg.content

为什么需要 MAX_TURNS?

没有上限的话,LLM 可能陷入无限循环------比如连续调用 get_working_diff 10 次而不返回文本。MAX_TURNS=10 确保单轮用户输入最多自动执行 10 步操作,超时后提示用户重新说明需求。

2.3 Conversation History:让 Agent 记住上下文

Agent 需要"记忆"才能进行多轮对话。LLMClient 内部维护一个 messages 列表:

python 复制代码
class LLMClient:
    def __init__(self, api_key):
        self.client = OpenAI(api_key=api_key, base_url="https://api.deepseek.com")
        self.messages = []  # 整个会话的消息历史

    def add_message(self, role, content):
        """添加用户/助手/系统消息"""
        self.messages.append({"role": role, "content": content})

    def add_tool_result(self, tool_call_id, content):
        """添加工具执行结果(role='tool')"""
        self.messages.append({
            "role": "tool",
            "tool_call_id": tool_call_id,
            "content": content,
        })

会话历史的结构:

复制代码
messages = [
    {"role": "system", "content": AGENT_SYSTEM_PROMPT},
    {"role": "user", "content": "当前在哪个分支?"},
    {"role": "assistant", "content": None, "tool_calls": [...]},   # LLM 决定调工具
    {"role": "tool", "tool_call_id": "...", "content": "main"},    # 工具结果
    {"role": "assistant", "content": "当前在 main 分支"},          # 最终回复
    {"role": "user", "content": "帮我创建一个新分支 test"},
    ...  # 持续增长
]

每次调用 API 时,整个 messages 列表都发给 LLM,LLM 因此知道"之前说过什么、做过什么"。

2.4 tools.py:工具注册中心

tools.py 是新增模块,负责两件事:

  1. 工具定义:返回 OpenAI Function Calling 格式的工具列表
  2. 工具调度:根据工具名找到对应的 handler 执行
python 复制代码
# 工具定义(OpenAI Function Calling 格式)
def get_tool_definitions() -> list:
    return [
        {
            "type": "function",
            "function": {
                "name": "current_branch",
                "description": "查看当前所在的分支名",
                "parameters": {"type": "object", "properties": {}, "required": []}
            }
        },
        {
            "type": "function",
            "function": {
                "name": "create_branch",
                "description": "创建并切换到新分支",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string", "description": "新分支的名称"}
                    },
                    "required": ["name"]
                }
            }
        },
        # ... 共 17 个工具
    ]

# 工具调度(映射到实际函数)
def execute_tool(tool_name, arguments, api_key="", model="deepseek-chat"):
    handlers = {
        "current_branch": lambda: f"当前分支:{current_branch()}",
        "create_branch": lambda: create_branch(arguments["name"]),
        "switch_branch": lambda: switch_branch(arguments["name"]),
        # ...
    }
    handler = handlers.get(tool_name)
    if not handler:
        return f"[ERROR] unknown tool: {tool_name}"
    return handler()

这个调度模式的关键在于:handler 返回的是字符串 ,这个字符串会通过 add_tool_result() 回传给 LLM。LLM 看到结果后,决定下一步做什么------是继续调工具,还是回复用户。


三、新增工具详解

3.1 分支管理

工具 对应 Git 命令 说明
current_branch git rev-parse --abbrev-ref HEAD 查看当前所在分支
list_branches git branch -a 列出所有分支
create_branch(name) git checkout -b <name> 创建并切换到新分支
switch_branch(name) git switch <name> 切换到已有分支
delete_branch(name, force) git branch -d/-D <name> 删除分支

区分 create 和 switch :实战篇中 create_branch 同时做"创建 + 切换"。扩展篇新增 switch_branch 用于仅切换(git switch),更符合 Git 2.23+ 的推荐用法。

3.2 代码审查与提交

工具 说明
get_working_diff 获取所有变更(staged + unstaged + untracked)
generate_commit_message 根据 diff 生成 Conventional Commit message
commit_changes(message) 暂存所有变更并提交
branch_diff(branch) 查看某分支与当前分支的差异

提交流程(Agent 自动执行):

复制代码
用户:提交代码
Agent:→ 调 get_working_diff → 看到修改了哪些文件
       → 调 generate_commit_message → 得到 commit message
       → 回复你:
          变更内容:
          README.md | +10
          生成的 commit message:
          feat: 添加用户注册接口
          
          是否提交?(y/n)
用户:确认
Agent:→ 调 commit_changes → 提交成功

这个流程体现了 Agent Loop 的核心价值:一次用户输入触发 LLM 多次工具调用,中间不需要用户干预。只有最终的确认步骤才需要用户参与。

3.3 回溯与变更管理

工具 对应 Git 命令 安全机制
rollback_to_commit(hash, hard) git reset --soft/--hard <hash> soft 模式需无未提交变更;hard 直接执行(会丢失变更)
force_reset(hash) git reset --hard <hash> 无安全检查,执行前必须用户确认
discard_changes(path) git checkout -- <path> 不可恢复,需用户确认
show_commit_log(count) git log --oneline -<count> 只读操作,无安全风险

为什么分开 soft 和 hard?

rollback_to_commit 的 soft 模式会检查是否有未提交的变更------因为 soft reset 保留工作区内容,如果已有未提交变更会导致冲突。而 hard 模式(--hard)正是用来丢弃变更的,所以即使有未提交变更也应该能执行。force_reset 则完全不检查,是一个"我说了算"的逃生舱。

这在 LLM 的系统提示词里明确写了:

复制代码
对于 destructive 操作(force_reset、discard_changes、hard reset、force delete),
必须告知用户后果并获得明确确认后再调用工具。

3.4 Stash 暂存管理

工具 对应 Git 命令 说明
stash_push(message) git stash push -m <message> 暂存当前变更,工作区变干净
stash_pop git stash pop 恢复最近一次暂存
stash_list git stash list 查看所有暂存记录

Stash 是一个典型的多工具协作场景。用户说"我临时切一下分支"时,Agent 应自动判断是否需要先 stash:

复制代码
> 帮我切到 main 看一下东西,再回来
Agent:当前分支有未提交的变更,我先 stash 一下
       → stash_push("temp before switching to main")
       → switch_branch("main")
       → ... 用户看完 ...
你:好了切回来
Agent:→ switch_branch("feature")
       → stash_pop()

3.5 其他工具

工具 说明
git_status 查看仓库状态(git status --short --branch
merge_branch(target, allow_uncommitted) 合并分支,默认检查未提交变更

四、REPL 对话式交互的实现

4.1 REPL 循环设计

CLI 入口从 argparse 单命令改为主循环:

python 复制代码
def main():
    api_key = os.environ.get("DEEPSEEK_API_KEY")
    if not api_key:
        print("[ERROR] 请设置 DEEPSEEK_API_KEY")
        sys.exit(1)

    client = LLMClient(api_key=api_key, model=args.model)
    client.add_message("system", AGENT_SYSTEM_PROMPT)

    print("Git Agent 已启动(输入 /exit 退出,/help 查看帮助)")

    while True:
        user_input = input("\n> ").strip()

        if user_input == "/exit":
            break
        elif user_input == "/help":
            show_help()
            continue
        elif not user_input:
            continue
        elif user_input == "/clear":
            # 清空历史,但保留 system prompt
            client.messages = [client.messages[0]]
            print("对话历史已清空")
            continue

        response = run_agent_turn(client, user_input)
        print(f"\n{response}")

为什么用 / 开头做命令? 避免与自然语言冲突。/exit/help/clear 都是元操作,不走 Agent Loop。其中 /clear 在长时间的对话后特别有用------上下文窗口满了会导致模型忘记早期对话。

4.2 对话历史管理

随着对话进行,messages 列表不断增长。两个问题:

  1. Token 消耗增大:每次 API 调用都发送全部历史
  2. 上下文窗口溢出:DeepSeek 是 64K 上下文窗口

目前通过 /clear 手动清理。未来可以:

  • 自动截断:保留最近 N 轮对话
  • Token 计数:超过阈值时自动 summarize 历史

4.3 保留 CLI 模式

扩展篇保留了 --no-repl 参数,可以用单条命令模式:

bash 复制代码
commit-agent --no-repl "查看当前分支"
commit-agent --no-repl "创建一个分支叫 test"

这在脚本化和集成场景下有用,REPL 和 CLI 只是交互方式不同,后端 Agent 逻辑完全复用。


五、安全设计

5.1 三层安全机制

层级 机制 说明
L1 工具定义中的描述 在 tool.description 中注明"危险操作"
L2 System Prompt 规则 明确要求 LLM 在调用危险工具前先问用户
L3 函数内部安全检查 合并/soft 回溯前检查未提交变更

5.2 危险操作清单

以下操作在 System Prompt 中被标记为"需用户确认":

  • force_reset --- 丢弃所有未提交变更
  • discard_changes --- 丢弃本地修改(不可恢复)
  • rollback_to_commit 的 hard 模式 --- 回溯到历史版本
  • delete_branch 的 force 模式 --- 删除未合并的分支

5.3 状态检查

以下操作在函数层面有安全检查:

操作 检查条件 通过后 拒绝后
merge_branch has_uncommitted_changes() 执行 merge 返回错误提示
rollback_to_commit(soft) has_uncommitted_changes() 执行 reset --soft 返回错误提示
rollback_to_commit(hard) 不检查 直接执行 reset --hard ---

六、运行示例

6.1 启动

bash 复制代码
# 设置 API Key(Windows PowerShell)
$env:DEEPSEEK_API_KEY="sk-xxxx"

# 启动 REPL
commit-agent

6.2 会话示例

复制代码
Git Agent 已启动(输入 /exit 退出,/help 查看帮助)

> 当前在哪个分支?

当前分支:main

> 创建一个分支叫 feature/login

已创建并切换到分支:feature/login

> 查看最近的提交

提交历史:
abc1234 initial commit

> 审查我的代码变更

(Agent 调 get_working_diff)
变更内容:
login.py | +45  新增登录页面
api.py   | +20  新增登录接口

> 生成 commit message

(Agent 调 generate_commit_message)
[Commit Message] type=feat title=feat: 添加用户登录功能
body:
- 新增登录页面(邮箱+密码)
- 新增登录 API 接口
- 使用 JWT 进行身份认证

是否提交?(y/n)

> y

(Agent 调 commit_changes)
提交成功

> /clear

对话历史已清空

> 切到 main 分支

已切换到分支:main

> 合并 feature/login

合并成功

> /help

Git Agent 可用工具列表:
- get_working_diff: 获取代码变更
- current_branch: 查看当前分支
- create_branch: 创建分支
- switch_branch: 切换分支
- merge_branch: 合并分支
- show_commit_log: 查看提交历史
- ...(共 17 个工具)

> /exit

再见!

七、常见问题

7.1 LLM 不调用正确的工具

这是最常见的问题。可能的原因和对策:

原因 对策
工具描述不够清晰 让 description 更具体,比如加上"先让用户确认后再调用"
工具名称不直观 工具名应该让 LLM 一看就懂,避免缩写
重名或相似工具有歧义 区分度低的工具合并或改名

7.2 对话历史越来越长

随着对话进行,messages 列表持续增长,导致:

  1. API 调用变慢(token 增加)
  2. LLM 注意力分散(历史过长)

解决方法:

  • /clear 手动清理
  • 自动摘要历史(高级功能,需额外 LLM 调用)

7.3 Agent 陷入循环

LLM 连续多次调用工具而不返回回复。MAX_TURNS=10 防止无限循环。

7.4 Windows 编码

运行前设置环境变量避免终端编码问题:

powershell 复制代码
$env:PYTHONIOENCODING='utf-8'

八、与通用 AI 助手的对比

读完本文你可能会想:既然 Claude、DeepSeek、ChatGPT 这些通用 AI 助手也能读懂并执行 git 命令,为什么还要专门写一个 Agent?

两种方案有本质差异,适用不同场景。以下以本文的 Git Agent 与 Claude Code(命令行 AI 助手)为例对比:

8.1 优势对比

对比维度 Git Agent 通用 AI 助手(如 Claude Code)
工具编排 内置 17 个 Git 工具,LLM 自动选择、链式调用。一条「提交代码」可以触发 get_working_diffgenerate_commit_message → 用户确认 → commit_changes 共 4 步自动化流程 需要每步都输出终端命令让你确认,工具链不连续
交互效率 「帮我暂存、切分支、合并、再回来」--- 一句话触发 6 步操作,中间不打断你 通常每执行一个命令就问你要不要继续,高频操作体验割裂
确定性 工具是硬编码的,什么参数、什么返回值,LLM 只能调这些,不会跑偏 可以做任何事(包括不该做的),需要你全程盯着
专注度 只做 Git 相关操作,不会被带偏去写代码、查资料 功能太多,容易分心。你说「切到 main」,它可能顺便分析起代码来
无外部依赖 只要有 DeepSeek API Key 就能运行,离线可用 需要联网且依赖特定平台
可定制 直接改 Python 代码,加工具 10 分钟搞定 你无法修改它的行为逻辑

8.2 通用 AI 助手的优势

对比维度 Git Agent 通用 AI 助手
理解深度 识别指令靠关键词匹配(工具名 + 参数),逻辑固定的场景没问题 理解复杂语义:「把上周三之后那个改了登录页面的提交回退掉」------能解析时间 + 范围 + 操作意图
文件级操作 只能执行 git 命令,无法查看或修改文件内容 不只能 git diff,还能直接读文件、改代码、查引用------「这个 commit 改了哪些函数,帮我检查有没有漏调用的地方」
上下文理解 只能看到 git 命令的输出(diff、status、log 等文本) 能看到整个项目的文件结构、多个文件的内容、git 历史,给出综合分析
能力边界 只有 17 个工具,超出就报 unknown tool 可以执行任何终端命令,没有预设上限
零配置 需要自己部署、配 API Key、装依赖 开箱即用,不需要任何配置

8.3 选择建议

复制代码
你的 Agent = 遥控器:一键开电视、调音量、换台,快且准
通用 AI 助手 = 管家:能分析「今晚看什么好」,但调频道你得说一声

什么时候用你的 Agent:

  • 高频重复操作:每天提交代码、切分支、合并,形成肌肉记忆
  • 标准化流程:团队统一的提交流程(必须先 lint → 再测试 → 再提交)
  • 有固定规则:commit 必须符合 Conventional Commits、分支命名规范等
  • 团队共享:写好一个 Agent,团队所有人都能用,行为一致

什么时候用通用 AI 助手:

  • 复杂一次性操作:回滚到某个特定提交前先检查影响范围
  • 需要判断的任务:分析多个分支的差异、审查代码质量、定位 bug
  • 跨领域操作:不只改 Git,还要改代码、查文档、调配置
  • 探索性工作:不确定该做什么,需要 AI 给建议

8.4 也可以组合使用

两者不是二选一的关系。实际工作流中完全可以结合:

复制代码
日常开发 → 用你的 Git Agent 快速提交、切分支
遇到复杂问题 → 用通用 AI 助手分析影响范围、给建议
有了方案 → 回到 Git Agent 执行具体操作

打个比方:你用遥控器(Agent)换台看节目,但不确定看什么时,叫管家(通用 AI)过来推荐一下。两种工具各司其职。


九、总结

从实战篇到扩展篇,发生了什么变化?

维度 实战篇(v1) 扩展篇(v2)
工具数量 1 个 17 个
交互方式 commit-agent --stage 对话式 REPL
调用模式 单次调用 Agent Loop(最多 10 轮)
对话历史 messages 列表维护上下文
架构文件 5 个模块 新增 tools.py
安全机制 三层防护 + 状态检查

为什么自然语言比终端命令更好?

  1. 降低认知负荷:不需要记住命令和参数,直接说话就行
  2. 组合操作一步到位:一句话 = 多条 git 命令
  3. 容错性强:LLM 理解同义表达,"切到 main"和"切换到 main 分支"都行
  4. 有上下文:Agent 记住"之前做了什么",不需要重复说明
  5. 安全 :危险操作有确认机制,不会像 git reset --hard 那样不可挽回

项目的完整代码

所有代码在 git-commit-agent/ 目录下:

复制代码
commit_agent/
├── __init__.py
├── cli.py          # REPL 入口 + 命令解析
├── agent.py        # Agent Loop 核心
├── git_utils.py    # 17 个 Git 操作封装
├── llm_client.py   # DeepSeek API + 对话历史
├── tools.py        # 工具定义 + 调度 (NEW)
└── prompts.py      # 多工具系统提示词
tests/
├── test_commit_agent.py   # 46 个测试
quick_test.py              # 快速测试脚本 (9 个测试)

讨论

完成该项目后,读者可能会有疑问,尽管我在文中写了当前的Agent与通用AI 助手的对比,但实际上,通用AI助手(如Claude Code)可以轻松完成当前Agent的开发,这就让目前的工作显得非常无意义,也让Agent开发显得有些没有价值

但是,真正的agent开发不是做这个层面的工作,写几个tool definition + 一个 system prompt就能跑的agent,只能作为Agent的Hello World;后续的内容还有很多,包括基础设施的搭建,安全与治理,Agent能力的测试与评估,此外还需要集成入现有的系统。

这些东西,不是写一个 system prompt 就能解决的。它们是工程问题,需要一整套架构和持续的维护。因为,还是有必须继续学习的。