Why we chose LangGraph to build our coding agent
为什么我们选择 LangGraph 来构建我们的编码 Agent
我们自 GPT-3 时代以来一直在 Qodo 构建 AI 编码助手。最初的方法是高度结构化的,为不同的编码任务(如测试生成、代码审查和改进)预定义了流程。这种方法在早期版本的 LLM 上效果很好,通过结构化的流程,我们能够从旧模型中获得实际价值,尽管它们存在各种限制。
自从 9 个月前 Claude Sonnet 3.5 发布以来,LLM 在通用编码任务方面的能力有了显著提高。 新的模型开启了构建更动态和灵活的工具的可能性,同时保持我们对代码质量的标准。我们希望摆脱僵化的工作流程,转向能够适应任何类型的用户请求的 Agent,同时仍然反映我们对如何最好地利用 AI 进行编码的观点。
最初,我们需要一个框架来快速验证我们的想法。在大约 4 个月前可用的几个选项中,我们选择了 LangGraph 作为我们最初的概念验证。我们惊喜地发现,该框架已经证明足够灵活和成熟,能够一路支持我们到生产环境。
在这篇文章中,我将解释为什么 LangGraph 是我们的正确选择,以及它如何使我们能够构建一个在灵活性与我们对编码最佳实践的观点之间取得平衡的编码助手。
灵活地表达观点
我们的主要考虑因素是在保持适应性的同时创建具有明确观点的workflow。 LangGraph 采用基于图的方法,使您可以灵活地构建 Agent,这些Agent可以在完全开放式(您只需为 LLM 提供所有可用工具并让它在循环中运行)到完全结构化的确定性流程(就像我们最初使用的流程)之间的任何位置运行。
从本质上讲,LangGraph 允许您为您的 Agent 定义一个状态机。 您创建表示工作流程中离散步骤的节点,以及定义它们之间可能转换的边。每个节点都可以执行特定的功能——收集上下文、规划、生成代码或验证——而图结构决定了这些功能如何连接。
图中连接的密度对应于您的Agent的结构化或灵活程度。 连接较少的稀疏图对应于更严格、可预测的流程,其中每个步骤都恰好导致一个下一步。 具有许多互连的密集图使Agent可以更自由地选择其路径。
未来,功能更强大的模型可能最适合完全开放的方法。 但是即使使用当前最好的 LLM,当您引导他们解决问题时,您仍然会获得更好的结果。 如果您直接使用 LLM 进行编码,您可能已经开发了自己的workflow——例如分解问题、有策略地提供上下文、引导模型进行复杂的推理,以及在需要时进行回溯或迭代。
LangGraph 灵活性的好处在于,当发布新的、更强大的模型时,我们可以轻松地重新校准流程的结构化程度。
我们的主要流程遵循您可能熟悉的模式:首先,上下文收集节点从代码库中收集相关信息(以及通过 MCP 集成来自外部资源的信息); 接下来,计划节点将任务分解为可管理的步骤; 然后,执行节点生成实际代码; 最后,验证节点对照最佳实践和要求检查输出。当验证失败时,Agent 会带着具体的反馈循环回到执行阶段,而不是从头开始。
连贯的接口
当您构建一个复杂的系统时,框架应该简化而不是使您的工作复杂化。 LangGraph 的 API 正是这样做的。
以下是使用 LangGraph 实现的我们的主要workflow的简化版本:
from langgraph.graph import StateGraph, END
workflow = StateGraph(name="coding_assistant")
workflow.add_node("context_collector", collect_relevant_context)
workflow.add_node("task_planner", create_execution_plan)
workflow.add_node("task_executor", execute_plan)
workflow.add_node("validator", validate_output)
# Define flow between nodes
workflow.add_edge("context_collector", "task_planner")
workflow.add_edge("task_planner", "task_executor")
workflow.add_edge("task_executor", "validator")
# Conditional routing based on validation results
workflow.add_conditional_edges(
"validator",
should_revise,
{
True: "task_executor", # Loop back if revision needed
False: END # Complete if validation passes
}
)
graph = workflow.compile()
graph.invoke({"user_input": "build me a game like levelsio"})
这种声明式方法使代码几乎可以自我记录。 workflow定义直接反映了我们的概念图,这使得它易于推理和修改。
每个节点函数接收当前状态并返回对该状态的更新。 幕后没有发生魔法,只是简单的状态转换。
LangChain 因其过于复杂的抽象而受到很多批评,但该团队确实通过 LangGraph 接口进行了改进。它增加了足够的结构,而不会妨碍您或强迫您采用复杂的思维模型,并将您的 Agent 逻辑完全展示出来,而不是将其隐藏在抽象背后。
跨 workflow 可重用的组件
可重用性是将有价值的框架与一次性框架区分开来的因素。 LangGraph 使用的基于节点的架构在这方面非常出色。
我们的上下文收集节点就是一个很好的例子。 它处理从代码库中收集相关信息,并且几乎在每个流程中都使用它。 我们的验证节点也是如此,它检查代码质量并运行测试。 这些组件可以以最小的配置插入到不同的图中。
随着我们构建更多的流程,速度回报是巨大的。 我们正在构建专门的流程,例如 TDD,它们具有不同的结构,但重用了许多相同的节点,只是以不同的配置连接,并添加了一些专门的组件。
状态管理
采用正确框架最令人满意的一部分是,您可以直接获得有用的功能。 LangGraph 的内置状态管理就是一个完美的例子。
只需几行代码即可将持久性添加到我们的Agent:
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.postgres import PostgresSaver
workflow = StateGraph(name="coding_assistant")
...
...
checkpointer = PostgresSaver.from_conn_string(
"postgresql://user:password@localhost:5432/db"
)
checkpointer.setup()
graph = workflow.compile(checkpointer=checkpointer)
就是这样。 通过这个简单的添加,我们的整个workflow状态(包括收集的上下文、制定的计划和生成的代码)都会持久化到我们的 postgres 数据库中,而无需我们构建任何自定义基础设施。 还有 SQLite 和内存检查点,可以同样容易地添加。
真正酷的是,这不仅可以实现跨会话的基本持久性。 它支持检查点和分支点,因此您可以撤消和重播更改。
未来发展方向
虽然 LangGraph 已经成为我们 Agentic 流程的良好基础,但它并非没有挑战。 一个痛点是文档。 该框架发展非常迅速,文档有时不完整或过时。 维护者很棒,并且在 Slack 上反应非常迅速(感谢 Harrison 和 Nuno 提供的所有帮助:))。 如果您使用的是更新的和更小众的功能,请准备好可能需要直接与项目维护者沟通。
在开发非确定性的 LLM 驱动系统时,测试和模拟是一个巨大的挑战。 即使是相对简单的流程也很难重现。 我们的Agent与 IDE 广泛交互,这很难在自动化测试中模拟。 我们构建了一个模拟基本 IDE 操作的模拟存储库,但它不能完美地复制真实环境。 这在我们可以自动测试的内容和生产中发生的内容之间造成了差距。
例如,诸如“查找此函数的所有用法”之类的依赖于 IDE 语言服务器的操作特别难以模拟。 这迫使我们更多地依赖手动测试,而不是我们所希望的,这减慢了迭代周期。
成熟的框架倾向于为模拟和测试提供强大的基础设施。 我希望 LangGraph 随着时间的推移会在这些领域发展。