何为Agent开发

       普通的大模型调用很简单,发起一次简单的LLM Api调用即可。Agent区分于普通的LLM Api调用,其关键在于让无状态的LLM Api调用能好用,不仅能够像有状态一样有记忆,还能分析思考,正确执行。
       与之延伸而来就有所谓的「记忆(历史会话)管理」「工具调用(Mcp)」Agent开发范式reactplan&excute等),让LLM Api调用能够拆解问题,多次请求,共享会话,最终实现目标。
       想要模型能够结合实时知识,进而又出现RAG。基本能力开发好了,想节省token成本,需要利用好LLM 的qv cache,增大缓存占比,就要在「记忆管理」的基础上加上分层管理,区分长短期记忆,将系统提示词、工具列表、长期记忆(claude.md)等不改动的放到提示词前面,以便命中前缀缓存;还有后续在MCP的基础上提出的「Skill」概念,通过渐进式加载的方式减少工具调用token长度。
       随着对LLM的进一步利用,单一的Agent可能无法完成协作任务,于是又开始聚焦于 Muti-Agent 开发,引申出了A2A协议以便多Agent间的通信,并基于此区分各种模式(主从、顺序、协作等)…

Agent开发框架核心抽象

       Agent开发框架,就是方便开发者去实现上述Agent开发过程中涉及的相关场景的。从22年的chatGPT出现到现在的cursor、claude、codex广泛普及,AI的发展太迅速了,成熟的Agent开发框架也已经出现了不少,如langchainlanggraphspring-AI等,作为一个站在巨人肩膀的AI应用开发,时常抱着学习态度去了解这些开发框架的实现代码,抛开不同框架间的语言差异不谈,会发现各个框架的核心抽象都基本一致。

Model

       最基本的,一次LLM调用需要明确模型厂商的providerbaseUrl等信息,所以需要抽象出Model类,各家厂商模型协议不同,只在Model类中抽象出call方法,上层不关心具体实现。

Message / Prompt

       模型调用需要输入,输入可以是提示词,也可以是模型上一轮输出,提示词又可以进行分层,因此对 Message / Prompt 抽象,包含

  • SystemMessage(系统提示词)
  • UserMessage(用户提示词)
  • AssistantMessage(模型回复)
  • ToolMessage(工具执行结果)
  • PromptTemplate(提示词组装scheme),

Prompt 不是一段文本,而是一组可组合、可追踪、可插入上下文的结构化消息。

Tool

       模型不只是聊天,它要能做事,所以出现了 ToolMcp/Skill本质上就是一种Tool,模型通过 function calling / tool calling 选择工具,框架负责解析参数、执行工具、把结果塞回对话。

interface Tool {
  name: string
  description: string
  schema: JSONSchema
  call(input: unknown): Promise<unknown>
}

Chain / Runnable

       实际场景往往不是一次模型调用就能解决问题的,通常需要多个步骤,所以需要把步骤串起来。于是出现了 Chain / RunnableRunnable把各种执行组件都统一成:

input -> invoke/stream/batch -> output

把 AI 应用从“写业务代码调用模型”变成“声明一条数据处理管线”,类似于工作流,只适用于流程相对固定的场景,还算不上Agent。

Agent

       Agent = Model + Prompt + Tools + Loop。Agent 的关键是一个循环:

观察当前状态
-> 让模型决定下一步
-> 如果要调用工具,执行工具
-> 把工具结果加入上下文
-> 再让模型判断
-> 直到完成

       经典 ReAct 范式就是:Thought -> Action -> Observation -> Thought -> Final Answer:

class AgentExecutor {
  model: ChatModel
  tools: Tool[]
  memory: Memory

  async run(input) {
    while (!done) {
      const response = await model.call(messages)
      if (response.toolCalls) {
        const results = await runTools(response.toolCalls)
        messages.push(results)
      } else {
        return response
      }
    }
  }
}

RAG

       接着为了引入实时外部知识实现RAG,又出现了 Document / Retriever / VectorStore,具体过程涉及文档加载(DocumentLoader)、分隔(TextSplitter)、向量化(EmbeddingModel)、向量存储(VectorStore)、向量检索(Retriever)、重排序(Reranker)等。其中的 Retriever 的抽象很重要,因为上层 Agent 不应该知道底层是 MilvusPGVectorElasticsearchRedisChroma,还是普通 SQL。

interface Retriever {
  retrieve(query: string): Promise<Document[]>
}

Graph/Node

       在复杂场景下,可以将复杂任务拆解成子任务,由多个Agent串行/并行实现,因而基于此提出图(Graph)的概念。Graph每个节点(Node)是一个Agent(当然也可以是Agent之下更小的运行组件),如规划节点、检索节点、代码执行节点、审核节点、人类确认节点、总结节点、失败重试节点、分支路由节点等。

Checkpoint

       每个节点的状态记录通过Checkpoint存储,Checkpoint 保存的是图执行状态快照,如当前 thread id、当前 graph step、当前 state values、各 channel version、下一步待执行节点、历史状态等,让每个具体节点状态都变得可观测。

State

       节点间的共享元数据使用 State维护(所有节点共享同一份),每个节点读取 State,每返回 State 的增量更新。一个通用的State定义通常包含:

type AgentState = {
  messages: Message[]
  taskId: string
  plan?: string[]
  currentStep?: number
  retrievedDocs?: Document[]
  toolResults?: ToolResult[]
  finalAnswer?: string
}

Memory

       想要保存跨轮次、跨任务、跨会话的记忆信息,又引申出MemoryMemory可以细分出很多类型,如长期记忆、短期记忆、规则/流程记忆等。

Hooks/Advisor

       同时节点执行前、执行后可以添加一下Hooks/Advisor拦截操作,例如通过这些Hooks来进行人工确认。
       实际代码编写过程中,通常是先建出各个节点,然后再建图,添加边,把一个节点到下一个节点作为一条边,例如我参与开源项目spring-ai-aibaba中的一个例子:

/**
	 * 示例 1: 添加短期内存
	 */
	public static void addShortTermMemory(ChatClient.Builder chatClientBuilder) throws GraphStateException {
		// 创建内存检查点器
		MemorySaver checkpointer = new MemorySaver();

		SaverConfig saverConfig = SaverConfig.builder()
				.register(checkpointer)
				.build();

		// 定义状态策略
		KeyStrategyFactory keyStrategyFactory = () -> {
			Map<String, KeyStrategy> keyStrategyMap = new HashMap<>();
			keyStrategyMap.put("messages", new AppendStrategy());
			return keyStrategyMap;
		};

		// 创建聊天节点
		var chatNode = node_async(state -> {
			List<Map<String, String>> messages =
					(List<Map<String, String>>) state.value("messages").orElse(List.of());

			// 使用 ChatClient 调用 AI 模型
			ChatClient chatClient = chatClientBuilder.build();
			String response = chatClient.prompt()
					.user(messages.get(messages.size() - 1).get("content"))
					.call()
					.content();

			return Map.of("messages", List.of(
					Map.of("role", "assistant", "content", response)
			));
		});

		// 构建图
		StateGraph stateGraph = new StateGraph(keyStrategyFactory)
				.addNode("chat", chatNode)
				.addEdge(START, "chat")
				.addEdge("chat", END);

		// 编译图
		CompiledGraph graph = stateGraph.compile(
				CompileConfig.builder()
						.saverConfig(saverConfig)
						.build()
		);

		// 第一轮对话
		RunnableConfig config = RunnableConfig.builder()
				.threadId("conversation-1")
				.build();

		graph.invoke(Map.of("messages", List.of(
				Map.of("role", "user", "content", "你好!我是 Bob")
		)), config);

		// 第二轮对话(使用相同的 threadId)
		graph.invoke(Map.of("messages", List.of(
				Map.of("role", "user", "content", "我的名字是什么?")
		)), config);
		// AI 将能够记住之前的对话,回答 "Bob"
		System.out.println("Short-term memory example executed");
	}

A2A

       如果说Tool 是 Agent 调用函数,那么A2A 是 Agent 调用另一个 Agent。一个具有多Agent节点的Graph可以理解为本地的A2A 调用,当然也可以通过httprpc实现远程A2A 调用,例如配合nacos注册中心进行Agent级别的服务注册、发现。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐