Skip to content

通用 CodeAgent 的落地实践:从架构到关键设计点

引言

如何构建一个真正能在开发流程中派上用场的 CodeAgent?不是那种回答一次就结束的代码问答机器人,而是能够理解任务、拆解步骤、操作文件、运行测试、并在遇到问题时自主调整的开发助手。

这个问题看似简单,实际落地时却会遇到大量工程细节:Agent 的状态如何建模?工具该怎么设计才能让模型稳定调用?上下文窗口有限的情况下如何让 Agent "看到"足够的信息?当 Agent 的一次操作出错时,系统该如何恢复?

这篇文章试图分享我们在这个过程中积累的一些经验。技术选型上,我们使用的是 LangGraph 和 LangChain 1.0,配合 Textual TUI 和 MCP 工具协议。但我们更希望讨论的是设计思路本身——这些思路在其他框架和工具链下同样适用。

我们期望的 CodeAgent 是什么样的

在深入设计细节之前,有必要先明确我们想要构建的目标。

我们期望的 CodeAgent 能够围绕一个开发任务持续推进。它不是回答一个问题就结束,而是能够主动拆解任务、查阅代码、修改文件、运行验证、汇报进展。在这个过程中,它需要维护清晰的任务状态——能够解释自己目前在做什么、已经完成了哪些步骤、接下来打算怎么做。

同时,它与环境的交互必须是可控的。所有的读写操作都应该通过结构化的工具完成,而不是让模型直接输出 shell 命令然后盲目执行。这意味着需要有明确的权限边界和安全策略,让团队能够逐步建立对 Agent 的信任。

终端环境是一个非常典型的落地场景:SSH 到服务器排查问题、在 CI 环境调试失败的构建、在容器里定位异常。这些场景的共性是信息密度高、需要多轮交互、且对操作的准确性有较高要求。本文以终端场景为例展开,但整体设计力求保持通用性。

整体架构:三层分离的设计

从架构视角看,一个可复用的 CodeAgent 可以拆成三个相对独立的层次。

交互层负责与用户的直接沟通。它可以是 Textual 构建的终端界面,也可以是 Web 控制台或 IDE 插件。这一层的职责是展示对话、文件内容、终端输出和任务列表,同时接收用户的输入和确认。

Agent 编排层是系统的核心。它维护 Agent 的状态,决定何时调用哪个工具,在不同的处理节点之间流转控制。我们使用 LangGraph 来实现这一层。LangGraph 经过 Uber、LinkedIn、Klarna 等公司的生产验证,其核心能力包括持久化执行(durable execution)、短期记忆管理、人机协作模式以及流式输出。这些特性对于需要长时间运行的 CodeAgent 来说非常关键。

工具能力层是 Agent 与真实环境交互的唯一入口。文件系统、代码编辑器、Shell、Git、Issue 系统——这些能力通过统一的工具协议对外暴露。Agent 不直接"操作世界",而是通过工具完成所有交互。这种设计借鉴了 Claude 工具使用的核心理念:将工具定义与 Agent 逻辑分离,使得工具可以独立测试、复用,并在不同场景下灵活组合。

这种三层拆分带来了实际的工程收益。我们可以在不改动工具实现的前提下替换 Agent 框架或模型;可以在不修改 Agent 逻辑的情况下切换交互形式;不同项目也可以共享同一套工具层,实现多 Agent 共用基础能力。

Agent 的状态设计:不仅仅是消息历史

传统的聊天应用只需要维护一个消息列表。但 CodeAgent 需要的状态要丰富得多。

我们设计的状态结构包含几个核心部分:消息历史(包括用户输入、Agent 回复和工具调用结果)、当前的任务拆解(一个带有状态标记的 TODO 列表)、当前工作目录、正在聚焦的文件列表、当前高优先级子任务的描述,以及一个用于中间推理的草稿区。

Python
class CodingAgentState(TypedDict):
    messages: Annotated[List, add_messages]
    todos: List[TodoItem]
    cwd: str
    working_files: List[str]
    focus_task: str
    scratchpad: str

这种显式的状态建模让 Agent 能够在多轮交互中保持连续性。当用户说"继续之前的重构工作"时,Agent 可以从状态中恢复上下文,而不需要重新解析整个对话历史。草稿区则用于承载中间推理结果——这些信息可能不需要展示给用户,但在调试和回放时非常有价值。

LangGraph 1.0 将"显式状态 + 图式编排"作为核心设计理念。每个处理节点都以完整的状态作为输入,产出更新后的状态作为输出。这种模式天然支持状态的持久化、检查点和回放,对于长时间运行的任务来说意义重大。

决策循环:从 ReAct 到更细粒度的控制

Agent 的决策循环是整个系统的心脏。最直接的实现是 ReAct 模式:模型根据当前上下文决定下一步动作,执行后将结果加入上下文,然后重复这个过程直到任务完成。LangChain 1.0 在新版本中引入了 create_agent 接口,正是基于这个模式:给 LLM 访问一组工具的能力,调用模型,如果模型选择调用工具就执行并循环,如果不调用工具就结束。

Python
from langchain.agents import create_agent

agent = create_agent(model, tools)

这个接口底层基于 LangGraph 实现,继承了 LangGraph 的 agent runtime 能力——包括持久化执行、流式输出和人机协作模式。对于简单场景来说,这已经足够好用。

但在更复杂的 CodeAgent 场景下,我们发现将决策循环拆分为几个职责更清晰的节点会更加稳定。

Plan 节点负责根据用户请求和当前上下文生成或更新任务拆解。例如,当用户说"为这个服务添加健康检查接口"时,Plan 节点会将其拆解为:理解当前项目结构、定位服务入口、编写健康检查实现、补充测试用例、更新文档。

Decide 节点综合任务拆解、消息历史和最近的工具结果,决定当前应该执行什么动作。这个节点的输出是一个明确的意图:需要查看更多代码、需要修改文件、需要运行测试,还是需要向用户提问澄清。

Act 节点根据决策结果调用具体工具。这一步的实现相对直接,主要是参数构造和结果解析。

Reflect 节点对刚执行的动作做自检。这一步可能会触发额外的验证动作(例如只运行与修改相关的测试用例),并根据结果更新 TODO 状态。

这种拆分让每个节点的职责更加单一,也更容易调试和优化。当发现 Agent 在某类任务上表现不佳时,可以定位到具体是规划出了问题、决策出了问题,还是执行出了问题。

工具设计:让模型能够可靠地调用

工具是 Agent 与真实环境交互的桥梁。工具设计的质量直接决定了 Agent 行为的稳定性。

根据 Claude 工具使用的最佳实践,好的工具定义应该有清晰的名称和描述、明确的输入输出 schema,以及合理的错误处理。模型会根据工具描述来决定何时使用哪个工具、如何构造参数。描述越清晰,模型的调用就越准确。

在 CodeAgent 场景下,我们将工具分为几个类别。

项目结构工具帮助 Agent 理解代码库的整体布局。tree 工具返回目录结构,支持深度限制和忽略模式;list_files 按 glob 模式返回文件列表。这类工具的设计要点是控制输出规模——代码库可能有成千上万个文件,工具需要能够返回有用的摘要信息,而不是把整个文件树塞进上下文。

内容检索工具让 Agent 能够精准定位代码。search_in_files 返回匹配的代码片段及其行号,而不是整个文件内容;read_file 支持指定起止行号,让 Agent 可以精细控制读取范围。这些工具需要处理好大文件的情况——当文件超过一定长度时,应该引导 Agent 使用搜索而不是全量读取。

编辑器工具是最需要谨慎设计的部分。我们采用了 Claude 文档中推荐的做法:编辑操作基于唯一性检查的字符串替换,而不是简单的行号定位。当要替换的字符串在文件中出现多次时,工具会返回错误并引导 Agent 先缩小范围。这种设计避免了"误伤"的问题。

执行工具提供 Shell 访问能力。这里的关键是维护持久化的 Shell 会话(保持工作目录和环境变量),同时处理好超时、输出截断和控制字符过滤。我们还引入了命令白名单机制——对于明显危险的操作(如 rm -rf /)在工具层直接拒绝。

外部系统工具通过 MCP(Model Context Protocol)接入 Git、Issue 系统、监控平台等。这类工具的读写能力通常需要分级控制:默认只读,在明确需要时才开放写操作。

一个容易被忽略的设计点是工具的错误信息。当工具调用失败时,返回的错误信息应该足够具体,让模型能够理解问题并调整策略。例如,"文件不存在"比"操作失败"更有用,"找到 3 处匹配,无法确定替换目标"比"替换失败"更能引导模型做出正确的下一步。

上下文管理:让 Agent 看到该看的信息

代码库和相关文档的规模通常远超模型的上下文窗口。如何让 Agent 在有限的窗口内获得足够的信息,是一个需要持续优化的问题。

我们采用的策略是将上下文分为三个层次。

任务上下文包含当前的用户目标、任务拆解和历史决策。这部分信息相对紧凑,由 Agent 状态直接承载,在每次调用时都会包含。

局部代码上下文是当前正在编辑或分析的代码片段。这部分内容通过工具调用动态获取,随着 Agent 的关注点移动而变化。关键是控制粒度——不需要整个文件,只需要相关的函数和调用链。

长期语义上下文包含架构文档、代码规范、历史讨论等。这部分内容通常通过检索(RAG 或工具调用)按需引入。它们的价值在于提供背景知识,而不是每次都需要出现在上下文中。

在构建每次模型调用的上下文时,我们遵循一个简单的流程:先通过工具或向量检索获取候选内容,然后按相关度和大小进行过滤,对冗长的文档做摘要处理,最后以稳定的模板组织成最终的 prompt。这个模板通常包含任务描述、当前 TODO 状态、相关代码片段和最近一次执行结果。

LangChain Core 在 1.0 版本中引入了 content blocks 的概念,为消息内容提供了更结构化的表示。这与现代 LLM API 返回结构化内容块的趋势一致,也让上下文的组织和解析变得更加清晰。

错误处理与恢复:Agent 不可能永远正确

在生产环境中运行的 CodeAgent 必然会遇到各种错误:工具调用失败、模型产生幻觉、任务理解偏差、外部系统不可用。设计一个健壮的错误处理机制是系统可靠性的关键。

工具级别的错误处理是第一道防线。每个工具都应该有清晰的错误类型定义和信息描述。当文件不存在、命令超时、权限不足时,工具应该返回结构化的错误信息,而不是让异常直接传播。Agent 可以根据这些信息决定是调整参数重试、切换到其他工具,还是向用户求助。

Agent 级别的自我纠错需要在决策循环中显式设计。当连续多次工具调用失败时,Agent 应该能够退后一步重新评估策略,而不是陷入无限重试的循环。这可以通过在状态中维护失败计数、在 Reflect 节点检查执行结果来实现。

任务级别的中断与恢复依赖于 LangGraph 的持久化执行能力。当系统因为外部原因中断时(网络问题、服务重启),Agent 的状态应该能够被保存并在之后恢复。LangGraph 1.0 的 durable execution 正是为这类场景设计的。

一个实用的做法是在 Agent 的系统提示中明确告知错误处理策略。例如:当工具调用失败时,优先根据错误信息调整调用方式;当连续三次失败时,暂停执行并向用户说明遇到的问题和尝试过的方法。这种显式的约束比期望模型"自己想明白"要可靠得多。

人机协作边界:何时需要人介入

Agent 不是要完全取代人,而是要与人高效协作。明确什么时候需要人介入、以什么方式介入,是落地时必须考虑的问题。

我们采用的模式是渐进式授权。Agent 默认以只读模式运行,可以自由地浏览代码、运行分析工具、查看测试结果。当需要修改文件时,Agent 会生成 diff 并等待用户确认。当需要执行可能有副作用的操作时(如提交代码、发布版本),Agent 会明确说明将要执行的操作并请求批准。

LangGraph 提供了内置的 human-in-the-loop 模式来支持这种场景。在图的定义中,可以将特定节点标记为需要人工确认,框架会自动处理等待和恢复的逻辑。

另一个重要的场景是澄清与求助。当 Agent 对任务理解存在歧义、当工具无法提供足够的信息、当决策涉及业务判断时,Agent 应该主动向用户提问,而不是基于假设继续执行。这需要在系统提示中明确鼓励这种行为,并在状态设计中支持等待用户输入的中间状态。

可观察性:让 Agent 的行为透明可查

CodeAgent 的行为比传统应用更加复杂和不确定。具备充分的可观察性,是调试问题、优化效果和建立信任的基础。

会话追踪是最基础的能力。为每次会话分配唯一 ID,将该会话中的所有 LLM 调用和工具调用关联起来。记录每次调用的输入、输出、延迟和 token 消耗。这些数据不仅用于调试,也是持续优化的依据。

决策记录比原始日志更有价值。在关键节点记录 Agent 的"思考过程":它看到了什么信息、考虑了哪些选项、为什么选择了当前的行动。这些记录可以帮助理解 Agent 的行为逻辑,也是优化 prompt 的重要参考。

回放与对比是更高级的能力。利用 LangGraph 的检查点机制,可以将某次执行的完整轨迹保存下来,之后用不同的 prompt 或工具配置重新执行,对比效果差异。这在优化 Agent 行为时非常有用。

在交互层面,实时的状态展示可以显著提升用户体验。当 Agent 开始一次工具调用时,界面上立即显示简要说明("正在搜索相关代码"、"正在运行单元测试"),避免用户在长时间等待中感到困惑。

能力模块化:应对不同场景的需求

不同团队、不同项目对 CodeAgent 的需求差异很大。一个 Go 后端项目和一个 React 前端项目需要的工具和知识完全不同。将能力模块化,按需组合,是保持系统可维护性的关键。

我们的做法是将工具和 prompt 片段组织成能力模块。每个模块定义自己的工具集合、补充的系统提示和可能的状态扩展。例如,Go 服务开发模块包含 go testgo build 等执行工具,以及 Go 项目结构和测试规范的知识;前端开发模块包含 npm testlint 等工具,以及组件结构和样式规范的知识。

通过配置文件选择启用哪些模块,可以快速适配不同的项目。这也降低了初始接入成本:一开始只启用最核心的能力,在稳定后再逐步扩展。

这种设计思路与 LangChain 生态的理念一致。LangChain 核心库提供了统一的模型和工具抽象,各种集成(OpenAI、Anthropic、文件系统、Git 等)作为独立包存在,开发者按需引入。

成本与延迟的权衡

在生产环境中,成本和延迟是不可忽视的因素。每次 LLM 调用都有时间和金钱成本,工具调用也有自己的开销。

减少不必要的调用是最直接的优化。通过更好的 prompt 设计,让模型在一次调用中完成更多推理;通过更精确的工具,减少"试错式"的探索调用。状态中缓存已获取的信息,避免重复读取同一个文件。

选择合适的模型也很重要。对于简单的分类或判断任务,可能不需要最强的模型;对于复杂的规划和推理,则需要更强的能力。可以在编排层实现模型选择逻辑,根据任务类型动态切换。

异步与并行可以改善用户感知的延迟。当多个独立的工具调用可以并行执行时,不必等待每个完成后再发起下一个。流式输出让用户能够在模型生成过程中就看到部分结果,而不是等待完整响应。

从一个具体场景开始

通用 CodeAgent 的构建涉及的模块确实很多。但实际落地时,最有效的方式是从一个非常具体的场景开始。

选择一个团队真实遇到的、频率足够高的问题。例如:"帮助在某个服务仓库中定位并修复配置错误",或者"协助为新增的 API 补充单元测试"。在这个具体场景下,逐步补齐本文讨论的关键设计:简单但足够的状态、最小工具集、有限的上下文管理、基础的错误处理。

当 Agent 能够在这个局部场景中稳定工作时,再考虑扩展。增加新的工具、支持更复杂的任务类型、接入更多的外部系统。这种渐进式的方法比一开始就追求"通用"要务实得多。

如果你正在考虑在自己的项目中落地 CodeAgent,可以从列一张"典型任务清单"开始。这些任务反映了团队真实的痛点,也定义了 Agent 需要具备的能力边界。然后将每个任务映射到具体的状态字段、工具调用和交互流程,就能得到一个可执行的实施计划。

结语

构建一个真正能用的 CodeAgent,核心挑战不在于调用 LLM 的能力,而在于如何将 LLM 的能力可控地嵌入到工程系统中。这需要清晰的状态设计、稳健的工具实现、合理的上下文管理,以及对错误处理和人机协作边界的仔细考量。