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

0 阅读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",而是在一个充满不确定性的系统里,设计出足够可靠的架构,让模型的概率性能力在你能接受的误差范围内稳定运行。