一、从终端命令到自然语言
1.1 传统 CLI 的痛点
在扩展之前,我们只有一条命令:
commit-agent--stage这个命令只做一件事:生成 commit message 并提交。如果你想做其他操作——切换分支、查看日志、合并代码——你需要记住另一套命令:
gitswitch maingitlog--oneline-5gitmerge feature-xgitreset--softHEAD~1gitstash push-m"wip"这里有几个问题:
- 记忆成本高:每个操作有独立的命令、参数、标志,你需要记住它们的具体写法
- 组合操作麻烦:想"先 stash 当前工作,再切换到 main 分支,合并 feature,再切回来 pop"——需要 4 条命令串起来
- 错误成本高:
git reset --hard敲错了就是数据丢失 - 上下文割裂:每条命令独立执行,没有"状态",没有"之前发生了什么"
1.2 自然语言交互的优势
把 Git Agent 从"单条命令"升级为"对话式助手"后,上述问题全部消失:
| 对比维度 | 传统 CLI | 自然语言 Agent |
|---|---|---|
| 学习成本 | 需记忆命令和参数 | 直接说话就行 |
| 组合操作 | 多条命令手动串联 | 一句话 = 多步操作 |
| 错误防护 | 敲回车即执行,无回滚 | LLM 会先检查状态,危险操作需确认 |
| 上下文 | 无状态 | Agent 记住对话历史,知道"之前做了什么" |
| 模糊匹配 | 参数写错就失败 | “切到 main” 和 “切换到 main 分支” 都理解 |
举个例子:
CLI 方式:
gitstash push-m"temp work"gitcheckout maingitpull origin maingitcheckout featuregitstash popAgent 方式:
> 帮我暂存当前工作,切到 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):
defrun_agent_turn(client,user_input):client.add_message("user",user_input)tools=get_tool_definitions()for_inrange(MAX_TURNS):# MAX_TURNS = 10 防止无限循环response=client.send_with_tools(tools)msg=response.choices[0].messageifmsg.tool_calls:# 执行每个工具调用fortool_callinmsg.tool_calls:result=execute_tool(tool_call.function.name,args)client.add_tool_result(tool_call.id,result)# → 继续循环,LLM 基于工具结果再次决策else:# LLM 返回文本,本轮结束returnmsg.content为什么需要 MAX_TURNS?
没有上限的话,LLM 可能陷入无限循环——比如连续调用get_working_diff10 次而不返回文本。MAX_TURNS=10确保单轮用户输入最多自动执行 10 步操作,超时后提示用户重新说明需求。
2.3 Conversation History:让 Agent 记住上下文
Agent 需要"记忆"才能进行多轮对话。LLMClient内部维护一个messages列表:
classLLMClient:def__init__(self,api_key):self.client=OpenAI(api_key=api_key,base_url="https://api.deepseek.com")self.messages=[]# 整个会话的消息历史defadd_message(self,role,content):"""添加用户/助手/系统消息"""self.messages.append({"role":role,"content":content})defadd_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 执行
# 工具定义(OpenAI Function Calling 格式)defget_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 个工具]# 工具调度(映射到实际函数)defexecute_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)ifnothandler:returnf"[ERROR] unknown tool:{tool_name}"returnhandler()这个调度模式的关键在于: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 单命令改为主循环:
defmain():api_key=os.environ.get("DEEPSEEK_API_KEY")ifnotapi_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 查看帮助)")whileTrue:user_input=input("\n> ").strip()ifuser_input=="/exit":breakelifuser_input=="/help":show_help()continueelifnotuser_input:continueelifuser_input=="/clear":# 清空历史,但保留 system promptclient.messages=[client.messages[0]]print("对话历史已清空")continueresponse=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参数,可以用单条命令模式:
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 启动
# 设置 API Key(Windows PowerShell)$env:DEEPSEEK_API_KEY="sk-xxxx"# 启动 REPLcommit-agent6.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 编码
运行前设置环境变量避免终端编码问题:
$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 就能解决的。它们是工程问题,需要一整套架构和持续的维护。因为,还是有必须继续学习的。