第 3 章 · Tool Calling 与工具系统

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",不需要传任何额外信息。

这和 searchread_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_statusgit_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.ts

agent 看到两个文件被修改了。

第二步: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 查看变更。下一节会用一个完整的工作流把它们串起来。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page