Agent 开发本质上就是高级点的 CRUD

1,063 阅读10分钟

我正在开发 DocFlow,它是一个完整的 AI 全栈协同文档平台。该项目融合了多个技术栈,包括基于 Tiptap 的富文本编辑器、NestJs 后端服务、AI 集成功能和实时协作。在开发过程中,我积累了丰富的实战经验,涵盖了 Tiptap 的深度定制、性能优化和协作功能的实现等核心难点。

如果你对 AI 全栈开发、Tiptap 富文本编辑器定制或 DocFlow 项目的完整技术方案感兴趣,欢迎加我微信 yunmz777 进行私聊咨询,获取详细的技术分享和最佳实践。

如果你对 OpenClaw 也感兴趣,也欢迎添加我微信,我拉你进交流群

很多做了一段时间 Agent 开发的人,都会冒出一种奇怪的失落感——明明在做"最前沿的 AI 应用",但每天干的事情和普通后端开发没啥区别:读数据、调接口、存结果、处理格式。

这种感觉其实非常精准。说明你已经看穿了目前市面上大多数 AI 应用的真面目,不过是通过胶水代码连接不同数据源和服务的系统。

但如果只停在这里,会错过一个更重要的认识:Agent 和传统 CRUD 的根本差异,不在于你调了多少接口,而在于"决策权"交给了谁,以及你如何处理那些无法被穷举的"不确定性"。

核心逻辑的变化,从硬编码到概率推理

传统业务系统里,if-else 是确定性的。开发者在写代码时,就已经预判了所有可能的分支,逻辑路径在 deploy 那一刻就已经完全固化。

// 传统 CRUD:逻辑路径写死在代码里
if (order.status === "PAID") {
  await shipOrder(order.id);
} else if (order.status === "PENDING") {
  await sendReminder(order.userId);
} else {
  await logUnhandledStatus(order.status);
}

Agent 的逻辑根本不是这么运作的。你把工具的用途用自然语言描述出来,模型根据当前的对话上下文,概率性地判断要不要调用它、传什么参数进去。

const tools: ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "ship_order",
      description:
        "当用户明确表示已完成支付,或系统确认订单状态为已支付时,发起发货流程。若用户只是咨询发货进度,不应调用此工具。",
      parameters: {
        type: "object",
        properties: {
          orderId: { type: "string", description: "订单唯一标识符" },
          priority: {
            type: "string",
            enum: ["normal", "express"],
            description: "发货优先级,默认为 normal",
          },
        },
        required: ["orderId"],
      },
    },
  },
];

const response = await openai.chat.completions.create({
  model: "gpt-4.1",
  messages: conversationHistory,
  tools,
  tool_choice: "auto",
});

const toolCall = response.choices[0].message.tool_calls?.[0];
if (toolCall?.function.name === "ship_order") {
  const args = JSON.parse(toolCall.function.arguments) as {
    orderId: string;
    priority?: "normal" | "express";
  };
  await shipOrder(args.orderId, args.priority ?? "normal");
}

关键点就在这里:你最终还是要写代码处理返回值,但中间那个"判断用户意图、决定执行哪个操作"的过程,不再是你写的条件判断,而是 LLM 的语义理解。这一层"决策权的让渡",是两者最本质的差异。

更微妙的是,你写的那段 description 字符串,实际上变成了系统行为的一部分。传统 CRUD 里,逻辑写在代码里,可以被静态分析、单元测试覆盖。而 Agent 的行为边界,部分藏在自然语言里,测试难度和不确定性都随之上升。

接口协议的演变,从手动对接到工具标准化

在传统开发里,你对接一个外部 API,流程通常是:

  • 看文档,理解数据结构
  • 写类型定义,手动组装请求参数
  • 处理各种奇怪的错误码和返回格式
  • 维护一份内部的封装层,隔离外部变化

到了 Agent 开发,这套流程变成了:把 API 的能力定义成一份 JSON Schema,让模型"看懂"这个工具能做什么、需要什么参数。工具描述的质量,直接决定了模型调用的准确率。

Function Call 解决的是单个模型和工具的对接问题。MCPModel Context Protocol)则把这个过程彻底标准化——不管你用的是 ClaudeGPT,还是 DeepSeek,只要服务提供方遵循 MCP 协议,Agent 就可以像接入 USB 设备一样无缝使用外部能力,包括数据库、搜索服务、本地文件系统、甚至另一个 AI 服务。

从工程角度看,这确实只是"换了一套接口协议"。但标准化带来的意义在于:工具可以在不同的 Agent、不同的模型之间复用,工具市场得以形成,开发者不再需要为每一个 LLM 重新写一遍对接代码。

这个演进方向很像当年从 SOAP 到 REST,再到 OpenAPI 规范的历程。每一次标准化,都会让生态扩张一个量级。

为什么很多人觉得"换汤不换药"

LangChain 这类框架下,这种感觉会尤其强烈。原因主要有两个。

第一,工程化的碎片极多。现在做 Agent 开发,80% 的精力都在处理 ChatPromptTemplate 的格式化、OutputParser 的解析逻辑、Memory 的上下文维护、Runnable 的链路拼装。这些工作和处理数据库连接池、管理事务、封装 DAO 层没有本质区别,都是繁琐的工程细节,只是套了一层 AI 的外壳。

第二,大多数"Agent"其实只是语义路由器。用户问 A,调工具 A,返回结果。整个流程是线性的、确定性的,和传统的请求-响应模式几乎相同。更直白地说,这种 Agent 不过是把原来的 if-else 路由,换成了"让 LLM 来决定调哪个函数"。能力边界没有变,只是路由逻辑的实现方式变了。

这种形态的 Agent 当然像 CRUD,因为它本来就没有超出 CRUD 的范畴。

真正让 Agent 和 CRUD 拉开距离的,是当任务本身无法被预先拆解、执行路径需要在运行时动态生成的时候。

真正的差异,出现在多步推理和自我纠错里

当任务变得复杂,无法用一次工具调用解决时,Agent 的价值才开始显现。

以"分析两家公司最近两个季度的财报并写一份竞争格局对比"为例,传统系统需要你预先设计好完整的工作流:先搜索 A 公司财报,再搜索 B 公司财报,提取关键数据,写入临时存储,最后调用生成模块输出结论。每一步的路径、每一步的数据格式,都要在代码里明确指定。

而用 ReAct(Reasoning + Acting)模式,模型可以自主循环规划和执行,不需要你把执行路径完全写死:

20260311005919

上图展示了 ReAct 循环的完整链路:模型接到复杂任务后,先动态拆解步骤,再进入"调用工具 → 观察结果 → 决定下一步"的循环,遇到失败时自动纠错调整策略,直到任务完成才退出。

这里有两个在传统 CRUD 里很难实现的能力,值得单独拆出来看。

动态规划,执行路径在运行时生成

传统系统里,你写代码时就要把所有执行路径确定下来。遇到没预见到的情况,系统要么报错,要么走 fallback。

Agent 里,模型可以根据上一步的结果,动态决定下一步做什么。搜索接口返回空数据?换个关键词再搜一次。第一家公司的财报在 PDF 里、第二家在网页上?模型会自己选合适的工具去读,不需要你提前为这种"格式不统一"的情况写分支处理。

执行路径是在推理过程中生长出来的,不是在编码时被预先铸造的。

错误自愈,异常不再是需要穷举的边界情况

当工具报错时,Agent 可以把错误信息读回上下文,自己判断是参数格式问题、权限问题还是业务逻辑问题,然后决定下一步策略。

interface AgentMessage {
  role: "user" | "assistant" | "tool";
  content: string;
  tool_call_id?: string;
}

interface AgentState {
  messages: AgentMessage[];
  retryCount: number;
}

async function runReActLoop(
  initialMessages: AgentMessage[],
  maxRetries: number = 5,
): Promise<string> {
  const state: AgentState = {
    messages: [...initialMessages],
    retryCount: 0,
  };

  while (state.retryCount < maxRetries) {
    const response = await openai.chat.completions.create({
      model: "gpt-4.1",
      messages: state.messages,
      tools,
    });

    const message = response.choices[0].message;
    state.messages.push({
      role: "assistant",
      content: message.content ?? "",
    });

    if (!message.tool_calls || message.tool_calls.length === 0) {
      return message.content ?? "";
    }

    for (const toolCall of message.tool_calls) {
      try {
        const result = await executeTool(
          toolCall.function.name,
          JSON.parse(toolCall.function.arguments),
        );
        state.messages.push({
          role: "tool",
          tool_call_id: toolCall.id,
          content: JSON.stringify(result),
        });
      } catch (error) {
        // 把错误信息原样喂回给模型,让它自己决定怎么处理
        state.messages.push({
          role: "tool",
          tool_call_id: toolCall.id,
          content: `Error: ${(error as Error).message}`,
        });
        state.retryCount++;
      }
    }
  }

  throw new Error(`Agent 超过最大重试次数 ${maxRetries}`);
}

传统系统里要实现类似能力,需要提前把所有可能的错误类型都枚举出来,针对每种错误写对应的恢复逻辑。Agent 不需要——你只要把错误信息给到模型,它自己会推理出下一步。

这不是魔法,而是把"异常处理的决策逻辑"从代码迁移到了模型的推理能力上。

Agent 架构的三个层次

可以把目前的 Agent 开发用一个简单的分层来理解:

层次形态特征
语义路由层Prompt + 单次工具调用if-else 的 AI 替代品
动态执行层ReAct 循环、多步规划执行路径在运行时生成
自主代理层多 Agent 协作、持久状态接近"数字员工"形态

目前大多数 Agent 产品停留在第一层,少数进入第二层,真正做到第三层的极少。

停留在第一层的 Agent,很快会被更自动化的低代码平台取代,因为本质上它们就是把接口调用包了一层自然语言的壳。真正的门槛在于:

  • 如何设计高质量的 RAG 检索流水线,让模型在推理时拿到足够准确的上下文,而不是把无关信息一股脑塞进去
  • 如何构建高可靠的工具集,保证工具的输入输出契约稳定,减少模型在格式解析上消耗的额外推理
  • 如何管理 Agent 的执行状态,让长程任务在中途中断后可以从断点恢复,而不是从头重来
  • 如何设计多 Agent 的协作边界,让不同专长的子 Agent 各司其职,而不是把所有能力堆给一个 Agent

从 LangChain 到 LangGraph 的思维跃迁

如果你已经在用 LangChain,建议花时间研究一下 LangGraph。这不是简单的框架升级,而是一个思维模型的切换。

SequentialChain 的思路是:输入 → 步骤 A → 步骤 B → 输出。这个模型对线性任务很够用,但一旦遇到需要循环、分支、并行的场景,就会开始变扭曲。

LangGraph 把 Agent 看作一个有状态的图(Stateful Graph)。每个节点是一个处理单元,可以是 LLM 调用,也可以是工具执行,甚至是一个子 Agent;节点之间通过共享状态(State)传递数据,边可以是条件边,根据状态的值决定下一步跳到哪个节点。

import { StateGraph, END } from "@langchain/langgraph";

interface GraphState {
  messages: AgentMessage[];
  planSteps: string[];
  currentStep: number;
  isComplete: boolean;
}

const graph = new StateGraph<GraphState>({
  channels: {
    messages: { reducer: (a, b) => [...a, ...b] },
    planSteps: { default: () => [] },
    currentStep: { default: () => 0 },
    isComplete: { default: () => false },
  },
});

graph.addNode("planner", plannerNode);
graph.addNode("executor", executorNode);
graph.addNode("evaluator", evaluatorNode);

graph.addConditionalEdges("evaluator", (state) => {
  if (state.isComplete) return END;
  if (state.currentStep >= state.planSteps.length) return "planner";
  return "executor";
});

graph.setEntryPoint("planner");

在这个模型下,你会发现自己在设计的不是"代码流程",而是"决策拓扑"——哪些节点可以并行、哪些节点需要等待上游结果、失败时应该回溯到哪个检查点。这和传统的业务流程设计有相似之处,但多了一层:每个节点的行为可以由 LLM 的推理驱动,而不是硬编码的条件判断。

"Agent 开发是高级 CRUD"这个判断是对的。但停在这里,就像说"编程不过是操作内存"一样——技术上没错,却少了最关键的那层:你在这些机制之上,能搭出什么来。

CRUD 给你的是确定性和可控性,代价是所有路径都需要被预先设计。Agent 给你的是泛化能力和动态性,代价是不确定性和更高的调试难度。两者不是替代关系,而是在不同场景下各有优劣的工具。

真正难的不是"学会怎么调 API",而是在一个充满不确定性的系统里,设计出足够可靠的架构,让模型的概率性能力在你能接受的误差范围内稳定运行。