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

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

1.1 传统 CLI 的痛点

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

commit-agent --stage

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

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 方式:

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):

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 列表:

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 执行
# 工具定义(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_branchgit rev-parse --abbrev-ref HEAD查看当前所在分支
list_branchesgit 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_popgit stash pop恢复最近一次暂存
stash_listgit 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 单命令改为主循环:

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 参数,可以用单条命令模式:

commit-agent --no-repl "查看当前分支"
commit-agent --no-repl "创建一个分支叫 test"

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


五、安全设计

5.1 三层安全机制

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

5.2 危险操作清单

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

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

5.3 状态检查

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

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

六、运行示例

6.1 启动

# 设置 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 编码

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

$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 就能解决的。它们是工程问题,需要一整套架构和持续的维护。因为,还是有必须继续学习的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值