这篇我按"先跑起来、再讲取舍"的方式写《Agent怎么学?先做一个会暴露问题的真实项目》。概念会讲,但重点放在代码怎么组织、哪里容易踩坑。

摘要

最近团队里有人把 Codex 接进 CI/CD,结果一周内报了三个"AI 写坏了线上代码"的事故。我介入排查后发现,问题根本不是模型能力不够,而是 Agent 的工具调用、记忆管理和任务规划三个环节全在裸奔。

先说结论:个人试用阶段的 Agent 和团队协作阶段的 Agent,中间隔着一套工程化的记忆与规划机制。下面这篇,把我最近三个月踩过的坑和总结的取舍逻辑摊开说。

---

目录

  • Agent 的本质:不是聊天机器人,是"带工具的执行体"
  • 规划能力:把"一步到位"拆成"可验证的步骤"
  • 工具调用:最容易被低估的环节
  • 记忆系统:团队协作的分水岭
  • 失败恢复:Agent 的"容错机制"
  • 适用边界:什么时候不该用 Agent
  • 代码解释
  • 总结

Agent 的本质:不是聊天机器人,是"带工具的执行体"

文章插图 1

很多人对 Agent 的理解还停留在"能对话的 AI"。这个认知偏差是后面所有问题的根源。

我带团队做第一个真实项目时,需求很简单:让 AI 帮我自动部署前端代码到测试环境。听起来就是个 shell 命令调用对吧?结果第一版跑起来,Agent 连续调用了 12 次 curl,有 3 次因为网络超时没重试,2 次在错误的时间戳上部署了错误的分支,最后还有一个 case 它把测试环境的数据库连接串当成了生产环境给清了。

问题的本质在于:Agent 没有"我在做什么"的状态意识。它每次响应都是无状态的,不知道上一轮调用成功与否,也不知道当前任务进行到了哪个阶段。

这就是为什么个人试用和团队协作体验差距巨大的根本原因。个人用时,出错就重来,成本可以接受。团队协作时,一个 Agent 错误部署可能影响十几个人的进度。

---

规划能力:把"一步到位"拆成"可验证的步骤"

文章插图 2

规划能力差的 Agent,就像一个不知道自己在干什么的实习生。你给它一个目标,它会直接开始执行,但执行过程中经常跑偏。

我看过一个很典型的案例。有个团队让 Agent 做代码审查,提示词写的是:"检查这个 PR 的质量,给出改进建议"。Agent 收到后直接调用 lint 工具、跑测试、检查代码风格,然后输出了 47 条建议。其中 12 条是误报,8 条是无效建议,真正有价值的只有 3 条。

问题出在哪?没有中间验证环节。Agent 把"检查 PR"这个任务当成原子操作执行,而不是拆分成可验证的子步骤。

正确的规划应该长这样:

任务:检查 PR 质量
├── 步骤1:调用 git diff 获取变更内容(验证:变更行数>0)
├── 步骤2:根据变更类型选择检查策略(验证:策略匹配成功)
│   ├── 如果是 API 变更:调用 lint + 测试
│   └── 如果是配置变更:调用配置校验工具
├── 步骤3:汇总检查结果(验证:结果非空)
└── 步骤4:过滤误报,输出最终建议(验证:建议可执行)

每一步都有明确的验证标准。如果某一步失败,Agent 可以回退或调整策略,而不是继续往下跑。

我见过最简洁的规划实现是用 JSON 树状结构描述任务依赖。每个节点包含:任务描述、前置条件、执行工具、验证方式、失败处理策略。这种结构的好处是,即使 Agent 执行到一半出错,也能从最近的检查点恢复。

---

工具调用:最容易被低估的环节

工具调用听起来简单,不就是调个 API 吗?但团队协作场景下,工具调用的问题比你想的多得多。

真实案例:工具权限配置失误

我们团队接入 Claude Code 时,遇到了一个诡异的问题。Agent 在某些项目里能正常调用 git 命令,在另一些项目里却报权限错误。排查了整整两天,最后发现是工具调用时的工作目录不对。

Agent 在调用 git status 时,工作目录被设置成了 /tmp,而不是项目根目录。这导致所有 git 命令都报"not a git repository"错误。

根本原因:Agent 的工具调用上下文是独立的,每次调用都需要明确指定工作目录。如果不显式设置,默认值可能不是你期望的。

工具调用的三个关键问题

1. 工具参数校验

很多团队没有对工具参数做前置校验。比如一个部署工具,要求传入 --env 参数,但 Agent 经常忘记传或者传错值。

我的解决方案是在工具调用前加一层参数校验:

def validate_deploy_params(params: dict) -> tuple[bool, str]:
    """校验部署参数,返回 (是否通过, 错误信息)"""
    required_keys = ["env", "branch", "service"]
    missing = [k for k in required_keys if k not in params or not params[k]]
    if missing:
        return False, f"缺少必要参数: {', '.join(missing)}"

    if params["env"] not in ["dev", "staging", "prod"]:
        return False, f"无效的 env 参数: {params['env']}"

    if params["branch"] and not re.match(r"^[a-zA-Z0-9/_-]+$", params["branch"]):
        return False, f"无效的 branch 格式: {params['branch']}"

    return True, ""

这段代码看起来简单,但它把大量错误拦截在了工具调用之前,而不是让工具执行失败后再处理。

2. 工具调用结果解析

工具调用返回的结果格式往往是混乱的。有的返回 JSON,有的返回纯文本,有的包含大量日志噪音。

我见过一个团队直接用 LLM 解析工具输出,结果在输出格式稍微波动时就解析失败。后来我们加了个中间层,先用正则提取关键信息,再交给 LLM 做语义理解。

3. 工具调用超时和重试

团队协作场景下,工具调用超时是常态。网络波动、服务重启、并发限制都可能导致超时。

我的经验是:不要盲目重试。盲目重试会让问题更严重,比如重复部署、重复发消息。重试前应该先判断失败原因:

  • 网络超时:可以重试,间隔递增
  • 参数错误:不要重试,直接报错
  • 权限错误:不要重试,需要人工介入
  • 服务不可用:可以重试,但最多 3 次

---

CSDN资料领取方式

记忆系统:团队协作的分水岭

记忆系统是 Agent 从"玩具"变成"工具"的关键。个人用时,你不需要记忆,因为每次都是新对话。团队协作时,Agent 需要记住上下文,否则每次都要重新解释背景。

记忆的三个层次

短期记忆:当前对话的历史。这个最简单,就是把之前的对话记录传给模型。但要注意 token 限制,超过限制就要做压缩或截断。

长期记忆:跨对话的知识。比如项目架构、团队规范、历史决策。这个需要持久化存储,通常用向量数据库或结构化数据库。

工作记忆:当前任务的中间状态。比如正在部署哪个服务、上次失败的原因是什么。这个最容易出问题,因为很多团队没有显式维护它。

真实踩坑:工作记忆丢失

我们团队有一次部署失败,Agent 在失败后没有记录失败原因,而是直接重新开始。结果它又犯了同样的错误,又失败了。第三次时,运维同事手动干预才发现问题。

教训:工作记忆必须显式维护,不能依赖模型的"上下文理解"。每次任务失败后,应该把失败原因、当前状态、下一步动作写入工作记忆。

记忆检索的取舍

记忆不是越多越好。我们最初把所有历史对话都存入向量数据库,结果检索时召回了大量无关信息,干扰了 Agent 的判断。

后来我们加了个过滤层:只检索与当前任务相关的记忆。判断相关性用的是简单的关键词匹配 + 时间衰减,而不是复杂的语义检索。

---

失败恢复:Agent 的"容错机制"

失败恢复是团队协作场景下最容易被忽视的环节。个人用时,失败了就重来。团队协作时,失败可能有连锁反应。

失败类型和应对策略

可恢复失败:网络超时、临时服务不可用。应对:重试 + 退避。

不可恢复失败:参数错误、权限不足、资源不存在。应对:立即报错,通知人工介入。

语义失败:工具调用成功,但结果不符合预期。应对:让 Agent 重新分析结果,调整策略。

失败恢复的代码示例

async def execute_with_recovery(task: Task, max_retries: int = 3) -> Result:
    """带失败恢复的任务执行"""
    for attempt in range(max_retries):
        try:
            result = await execute_task(task)

            # 验证结果
            if not validate_result(result, task):
                raise ResultValidationError(f"结果不符合预期: {result}")

            return result

        except NetworkTimeoutError as e:
            if attempt < max_retries - 1:
                await asyncio.sleep(2 ** attempt)  # 指数退避
                continue
            raise

        except ValidationError as e:
            # 参数错误,不要重试
            logger.error(f"参数验证失败: {e}")
            return Result(error=str(e), retryable=False)

        except UnknownError as e:
            # 未知错误,记录日志后重试
            logger.warning(f"未知错误,attempt {attempt + 1}: {e}")
            if attempt < max_retries - 1:
                continue
            raise

这段代码的关键在于:区分可恢复和不可恢复的失败。不是所有失败都应该重试,盲目重试只会让问题更严重。

---

适用边界:什么时候不该用 Agent

Agent 不是万能的。以下几个场景,用传统脚本或人工处理可能更好:

确定性任务:如果任务逻辑固定、输入输出明确,用脚本更可靠。Agent 的"灵活性"在这里是负担。

高一致性要求:金融、医疗等领域,每次执行结果必须完全一致。Agent 的非确定性是风险。

简单 CRUD:如果任务只是增删改查,调 API 比让 Agent 规划执行路径更高效。

实时性要求高:Agent 的规划、工具调用、结果解析都有延迟,不适合实时场景。

我的判断标准很简单:如果任务可以用 if-else 写清楚,就不要用 Agent。Agent 的价值在于处理不确定性和复杂决策,而不是替代简单逻辑。

---

代码解释

下面对文中两个关键代码段做逐段拆解,讲清楚输入、核心逻辑、输出和异常处理。

1. 部署参数校验函数

def validate_deploy_params(params: dict) -> tuple[bool, str]:
    """校验部署参数,返回 (是否通过, 错误信息)"""
    required_keys = ["env", "branch", "service"]
    missing = [k for k in required_keys if k not in params or not params[k]]
    if missing:
        return False, f"缺少必要参数: {', '.join(missing)}"

    if params["env"] not in ["dev", "staging", "prod"]:
        return False, f"无效的 env 参数: {params['env']}"

    if params["branch"] and not re.match(r"^[a-zA-Z0-9/_-]+$", params["branch"])
        return False, f"无效的 branch 格式: {params['branch']}"

    return True, ""

输入:一个字典 params,包含部署所需的参数,如环境(env)、分支(branch)、服务名(service)。

核心逻辑:分三步校验。第一步检查必填字段是否缺失或为空;第二步校验 env 的值是否在允许的三个枚举值内;第三步用正则表达式校验 branch 的格式,允许字母、数字、斜杠、下划线和连字符。

输出:返回一个元组 (bool, str)。第一个值是布尔类型,表示校验是否通过;第二个值是错误信息字符串,校验通过时返回空字符串。

异常处理:这个函数本身不抛出异常,所有错误都以返回值的形式传递。调用方需要根据返回值决定是否继续执行部署。这种设计的好处是把错误拦截在工具调用之前,避免无效调用浪费资源。

2. 带失败恢复的任务执行函数

async def execute_with_recovery(task: Task, max_retries: int = 3) -> Result:
    """带失败恢复的任务执行"""
    for attempt in range(max_retries):
        try:
            result = await execute_task(task)

            # 验证结果
            if not validate_result(result, task):
                raise ResultValidationError(f"结果不符合预期: {result}")

            return result

        except NetworkTimeoutError as e:
            if attempt < max_retries - 1:
                await asyncio.sleep(2 ** attempt)  # 指数退避
                continue
            raise

        except ValidationError as e:
            # 参数错误,不要重试
            logger.error(f"参数验证失败: {e}")
            return Result(error=str(e), retryable=False)

        except UnknownError as e:
            # 未知错误,记录日志后重试
            logger.warning(f"未知错误,attempt {attempt + 1}: {e}")
            if attempt < max_retries - 1:
                continue
            raise

输入:task 是要执行的任务对象,max_retries 是最大重试次数,默认 3 次。

核心逻辑:用 for 循环控制重试次数。每次循环先执行任务,然后验证结果。如果结果不符合预期,抛出 ResultValidationError。根据异常类型分别处理:网络超时和未知错误会重试,参数验证错误直接返回失败结果。

输出:返回 Result 对象,包含执行结果或错误信息,以及 retryable 字段标识是否可重试。

异常处理:这里的关键设计是区分不同异常类型的处理策略。网络超时采用指数退避(2 attempt),避免频繁重试加重服务器负担。参数验证错误不重试,直接返回。未知错误在达到最大重试次数后重新抛出,让上层调用方处理。

---

总结

Agent 的三大核心——工具调用、记忆、规划——不是独立模块,而是一个整体。工具调用是手脚,记忆是大脑的存储,规划是决策过程。任何一个环节薄弱,都会导致协作场景下的失败。

个人试用和团队协作的分水岭,不在于模型能力,而在于工程化程度。记忆系统的完善、工具调用的校验、失败恢复的策略,这些"boring"的工程细节,才是 Agent 能否真正进生产环境的决定性因素。

最近团队里用 Claude Code 做代码审查,效率确实提升了。但前提是:我们把工具调用参数校验、工作记忆维护、失败恢复策略都补上了。这些工作占了总开发量的 60%,模型调参只占 10%。

这就是我的真实体会:Agent 的难点不在 AI,而在工程。

资料展示

下面是我整理的AI大模型学习资料和工具包预览,适合收藏后按主题逐步学习。

AI大模型资料展示 1

AI大模型资料展示 2

AI大模型资料展示 3

AI大模型资料展示 4

如果你想看完整资料目录,可以在评论区留言「资料」;也欢迎告诉我你更关注AI大模型里的哪类内容。

CSDN官方大礼包

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐