一、从终端命令到自然语言
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"
这里有几个问题:
- 记忆成本高:每个操作有独立的命令、参数、标志,你需要记住它们的具体写法
- 组合操作麻烦:想"先 stash 当前工作,再切换到 main 分支,合并 feature,再切回来 pop"------需要 4 条命令串起来
- 错误成本高 :
git reset --hard敲错了就是数据丢失 - 上下文割裂:每条命令独立执行,没有"状态",没有"之前发生了什么"
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.py 和 agent.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 是新增模块,负责两件事:
- 工具定义:返回 OpenAI Function Calling 格式的工具列表
- 工具调度:根据工具名找到对应的 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 列表不断增长。两个问题:
- Token 消耗增大:每次 API 调用都发送全部历史
- 上下文窗口溢出: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 列表持续增长,导致:
- API 调用变慢(token 增加)
- 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_diff → generate_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 |
| 安全机制 | 无 | 三层防护 + 状态检查 |
为什么自然语言比终端命令更好?
- 降低认知负荷:不需要记住命令和参数,直接说话就行
- 组合操作一步到位:一句话 = 多条 git 命令
- 容错性强:LLM 理解同义表达,"切到 main"和"切换到 main 分支"都行
- 有上下文:Agent 记住"之前做了什么",不需要重复说明
- 安全 :危险操作有确认机制,不会像
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 就能解决的。它们是工程问题,需要一整套架构和持续的维护。因为,还是有必须继续学习的。