05 · Git 工具:查看变更
git_status 查看哪些文件改了,git_diff 查看具体改了什么。让 agent 了解自己的修改影响。
为什么 agent 要知道 Git 状态
agent 用 patch_file 修改了代码,用 run_command 跑了测试。但它在改代码之前可能已经做了好几轮搜索和阅读,改的时候也可能不只改了一个文件。当用户问"你改了什么"的时候,agent 需要一个清晰的总览。
更重要的是,agent 自己也需要这个信息。修改完代码后,agent 应该检查自己的修改是否只影响了预期的文件。如果 git status 显示有三个文件被改了,但 agent 只记得改了一个,那可能有什么地方出了问题。
两个 Git 工具各有分工:
- git_status:回答"哪些文件改了"
- git_diff:回答"具体改了什么"
它们是只读工具——agent 可以查看变更,但不能提交代码。commit、push 这些写操作要留给后面的章节。
git_status:一眼看全貌
// src/tools/git-status.ts
import { exec } from "node:child_process";
import { promisify } from "node:util";
import type { AgentState, Tool } from "../types";
const execAsync = promisify(exec);
/**
* git status 工具
*
* 让 agent 了解当前仓库的状态:哪些文件改了、有没有未追踪的文件。
* 这是 agent 在修改代码后进行验证的基础。
*/
export const gitStatusTool: Tool = {
name: "git_status",
description:
"查看当前仓库的 git 状态。" +
"返回修改、暂存、未追踪的文件列表。" +
"适合在修改文件后检查变更情况。",
parameters: {
type: "object",
properties: {},
required: [],
},
async execute(
_args: Record<string, unknown>,
state: AgentState,
): Promise<string> {
try {
const { stdout } = await execAsync("git status --short", {
cwd: state.workingDir,
});
if (!stdout.trim()) {
return "工作目录干净,没有未提交的变更。";
}
return `当前变更:\n${stdout.trim()}`;
} catch (error: unknown) {
return `获取 git 状态出错:${String(error)}`;
}
},
};实现非常简洁,核心就是一行命令:git status --short。
--short 的输出格式
git status --short 的每一行都是一个文件的状态标记加路径:
M src/utils/math.ts /** M = 已修改(未暂存) */
A src/utils/new.ts /** A = 已添加到暂存区 */
?? src/utils/draft.ts /** ?? = 未追踪的新文件 */
D src/utils/old.ts /** D = 已删除 */两列状态标记:左列表示暂存区状态,右列表示工作区状态。 M 表示工作区修改了但还没暂存,A 表示已经 add 到暂存区。agent 只需要知道"哪些文件有变化",不需要深入理解 Git 的暂存机制。
空结果也有明确语义。 stdout 为空意味着工作目录干净——没有任何修改。返回"工作目录干净,没有未提交的变更"而不是空字符串,让模型清楚知道"确实没有变更"而非"查询出了问题"。
无参数工具
注意 git_status 的参数定义:
parameters: {
type: "object",
properties: {},
required: [],
},properties 是空对象,required 是空数组。这意味着这个工具不需要任何参数——模型只需要说"调用 git_status",不需要传任何额外信息。
这和 search、read_file 等需要参数的工具形成对比。无参数工具在 Agent Loop 中的调用更轻量:模型不需要构造参数,只需要一个工具名就够了。
git_diff:看到每一行改动
git_status 告诉你"改了哪些文件",但不知道具体改了什么。git_diff 补上了这个信息:
// src/tools/git-diff.ts
import { exec } from "node:child_process";
import { promisify } from "node:util";
import type { AgentState, Tool } from "../types";
const execAsync = promisify(exec);
/** diff 输出最大长度 */
const MAX_DIFF_LENGTH = 10_000;
/**
* git diff 工具
*
* 让 agent 查看具体的代码变更内容。
* 和 git_status 互补:status 告诉你"哪些文件改了",diff 告诉你"改了什么"。
*/
export const gitDiffTool: Tool = {
name: "git_diff",
description:
"查看当前仓库的代码变更详情(git diff)。" +
"返回具体的增删行内容。" +
"可以通过 path 参数查看指定文件的变更。",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "可选,只查看指定文件的变更",
},
staged: {
type: "boolean",
description: "是否查看暂存区的变更(git diff --staged),默认 false",
},
},
required: [],
},
async execute(
args: Record<string, unknown>,
state: AgentState,
): Promise<string> {
const staged = args.staged === true;
const path = args.path ? String(args.path) : undefined;
const parts = ["git diff"];
if (staged) parts.push("--staged");
if (path) parts.push("--", path);
try {
const { stdout } = await execAsync(parts.join(" "), {
cwd: state.workingDir,
maxBuffer: 1024 * 1024,
});
if (!stdout.trim()) {
return staged ? "暂存区没有变更。" : "没有未暂存的变更。";
}
if (stdout.length > MAX_DIFF_LENGTH) {
const truncated = stdout.slice(0, MAX_DIFF_LENGTH);
return `${truncated}\n\n... diff 输出过长,已截断(共 ${stdout.length} 字符)`;
}
return stdout;
} catch (error: unknown) {
return `获取 git diff 出错:${String(error)}`;
}
},
};参数设计:path 和 staged
git_diff 提供了两个可选参数:
path:只查看指定文件的变更。当 git_status 显示多个文件被修改时,agent 可以用 path 参数逐个查看每个文件的变更详情,而不是一次性拿到所有文件的 diff。
staged:查看暂存区的变更。默认查看工作区变更(未 git add 的部分),设为 true 时查看已经 git add 的部分。
命令拼接的逻辑很直观:
const parts = ["git diff"];
if (staged) parts.push("--staged");
if (path) parts.push("--", path);
const { stdout } = await execAsync(parts.join(" "), {
cwd: state.workingDir,
maxBuffer: 1024 * 1024,
});注意 -- 的使用。git diff -- path/to/file 中的 -- 分隔选项和路径,防止路径被误认为 Git 选项(比如文件名以 - 开头的极端情况)。这是 Git 命令行的一个好习惯。
输出截断
和 run_command 一样,git_diff 也有输出长度限制:
/** diff 输出最大长度 */
const MAX_DIFF_LENGTH = 10_000;
if (stdout.length > MAX_DIFF_LENGTH) {
const truncated = stdout.slice(0, MAX_DIFF_LENGTH);
return `${truncated}\n\n... diff 输出过长,已截断(共 ${stdout.length} 字符)`;
}diff 输出可能很长——一个大文件的重命名或重构可能产生几千行的 diff。10,000 字符的限制确保不会把过长的内容塞进上下文窗口。
截断提示告诉模型两件事:输出被截断了("已截断"),以及原始输出有多长("共 N 字符")。模型据此判断是否需要用 path 参数缩小查看范围。
空结果的语义区分
if (!stdout.trim()) {
return staged ? "暂存区没有变更。" : "没有未暂存的变更。";
}空 diff 不一定是"什么都没改"——可能只是改动已经全部 git add 了。根据 staged 参数返回不同的消息,帮助模型准确理解当前状态。
staged = false,空输出:"没有未暂存的变更"——所有改动都已经 add 了staged = true,空输出:"暂存区没有变更"——还没 add 过任何东西
这种区分看起来细微,但对模型判断"现在该做什么"很重要。
只读 Git 工具的设计意图
git_status 和 git_diff 都有一个共同特点:它们只读取信息,不做任何修改。
这不是技术限制——用 execAsync("git add .") 或 execAsync("git commit -m 'xxx'") 在实现上没有任何区别。这是有意的功能边界划分:
| 操作 | 工具 | 风险等级 |
|---|---|---|
| 查看状态 | git_status | 只读,零风险 |
| 查看变更 | git_diff | 只读,零风险 |
| git add | 暂不提供 | 低风险但影响仓库状态 |
| git commit | 暂不提供 | 高风险,创建不可逆的提交 |
| git push | 暂不提供 | 极高风险,影响远程仓库 |
先给 agent 只读能力,让它在修改代码后能自我检查。写操作(commit、push)属于需要权限控制的高风险操作,放到权限系统中去管理。
git_status 和 git_diff 的配合
两个工具典型的工作流:
第一步:git_status 看全貌
> git_status
当前变更:
M src/utils/math.ts
M src/utils/math.test.tsagent 看到两个文件被修改了。
第二步:git_diff 看详情
> git_diff(path: "src/utils/math.ts")
diff --git a/src/utils/math.ts b/src/utils/math.ts
index 3a1b2c4..5d6e7f8 100644
--- a/src/utils/math.ts
+++ b/src/utils/math.ts
@@ -1,5 +1,5 @@
export function add(a: number, b: number): number {
- return a - b;
+ return a + b;
}agent 看到具体把 - 改成了 +,确认修改正确。
第三步:如果变更太多,用 path 逐个查看
当 git_status 显示多个文件被修改时,agent 会用 path 参数逐个查看每个文件的具体变更,而不是一次性拿到所有 diff。这是一种"先总览、再细节"的检查策略。
注册 Git 工具
更新工具注册表:
// src/tools/index.ts
import { gitStatusTool } from "./git-status";
import { gitDiffTool } from "./git-diff";
export const tools: Tool[] = [
searchTool,
globTool,
readFileTool,
writeFileTool,
patchFileTool,
runCommandTool,
gitStatusTool,
gitDiffTool,
];Git 工具放在最后——它们是"验证层",在搜索、读写、执行命令之后使用。
到这一步,八个基础工具全部注册完毕。agent 有了完整的工具链:搜索找代码、读写改文件、执行命令跑验证、Git 查看变更。下一节会用一个完整的工作流把它们串起来。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。