news 2026/9/7 1:30:21

AI Agent开发实战:从手写循环到企业级工程化落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI Agent开发实战:从手写循环到企业级工程化落地

AI Agent开发在2026年已经不只是大模型厂商的专属话题,而是后端工程师、算法工程师和运维团队都要面对的实际工程问题。很多人已经不再问“大模型能做什么”,而是开始问“怎么让大模型在自己的系统里稳定地调用工具、查数据、做决策、处理异常”。这篇文章围绕“从零基础到企业级项目实战”这条主线,把 AI Agent 开发的完整链路讲透:先理解 Agent 的核心运行逻辑,再搭建可复现的开发环境,然后分别用原生代码和主流框架实现一个可运行的 Agent,接着补上 Spring Boot 侧客户端集成、运行验证、故障排查和生产化改造。整篇内容都以工程落地为目标,不写概念空谈,也不堆没有出处的资料结论。

如果你之前只调用过单次大模型接口,或者只做过 Prompt 工程,那么这篇文章会把两者之间的那道“工程鸿沟”补上。学完后你不仅能写一个最小可运行的 Agent 示例,还能知道它为什么这么设计、上线前还要处理哪些问题。

1. 先理解 AI Agent 的运行逻辑,再看代码

1.1 Agent 到底是什么

用一句话概括:AI Agent 是一种“能自主拆解任务、调用外部工具、观察执行结果、调整下一步动作”的大模型应用。它和普通的大模型对话程序最大的区别,是它有一个行动循环

传统的大模型调用是“问一句、答一句”。Agent 则更像是“领到一个目标,自己拆分子步骤,完成一步、检查一步、再决定下一步”。比如用户问“帮我查一下订单状态,并解释为什么需要补差价”,单次 Prompt 调用可能只能根据上下文给出一个笼统回答;而 Agent 可以先调用订单查询工具拿到真实订单数据,再根据数据解释差价原因,如果数据缺失还能自动追问用户或查另一个系统。

技术定义上,Agent 不是单一模型,而是由以下要素组成的程序:

  • 大模型:负责理解任务、生成规划、决策下一步动作。
  • 工具注册表:把外部能力(数据库查询、HTTP 接口、文件操作、内部 API)暴露给模型。
  • 记忆:短期上下文和工作区状态、长期知识库或外部存储。
  • 执行循环:模型输出决策,程序执行工具,把结果再交给模型,直到任务完成或达到上限。
  • 安全边界:限制模型能访问的工具、数据和操作范围。

在实际项目中,Agent 更像一个“智能班长”:它自己做不完所有事,但知道去哪里问、找谁做、怎么判断结果靠不靠谱。

1.2 Agent 与单次大模型调用的关键区别

下面这张表可以快速理解为什么不能只在单次 Prompt 里“硬塞”所有逻辑:

对比维度单次 Prompt 调用AI Agent
任务目标回答当前问题完成一个可能包含多步骤的目标
外部数据依赖上下文中已经存在的文本可以主动调用工具获取实时数据
工具调用不涉及或只做格式要求需要工具定义、参数解析、结果回填
错误处理回答错误后,用户重新提问Agent 可以观察工具结果并自动重试或换策略
上下文控制一次请求写完,超长就截断需要管理多轮消息、工具结果和状态
成本控制单次请求消耗可预估多轮循环可能导致成本放大,必须设上限
工程复杂度较低,适合简单问答需要设计循环、退出条件、日志、权限和评估

这个区别决定了 Agent 开发的核心难点:不是模型有多聪明,而是你的工程系统能不能承接模型不断变化的输出

1.3 Agent 的典型工作链路:规划、工具、记忆、反思

运行一个 Agent 时,后台通常会发生这样一条链路:

  1. 理解目标:模型读取系统提示词和用户请求,明确要完成什么。
  2. 拆解计划:模型决定需要调用哪个工具、传什么参数、先做哪一步。
  3. 程序执行:你的代码而不是模型本身去调用真实工具,拿到结构化结果。
  4. 观察结果:工具结果作为新消息回传给模型,模型判断结果是否符合预期。
  5. 继续或结束:如果还不够,再次调用工具;如果已经完成,生成最终回复。

这里面最容易被忽略的是“记忆”和“反思”。

短期记忆指当前会话上下文中的用户输入、工具调用和返回结果。长期记忆指跨会话的用户偏好、历史结论和知识库内容。反思则是指模型在拿到工具结果后,不是无条件接受,而是判断“这个结果是对的吗?够吗?需要再查一个数据吗?”。

有的 Agent 框架把反思做成了显式节点,比如“在回答前先验证计算结果是否合理”。在自研 Agent 里,这种反思逻辑可以靠 Prompt 引导实现,也可以靠代码规则实现。结论是:不要把所有判断都交给模型,能用代码判断的边界尽量用代码。

1.4 什么场景适合用 Agent,什么场景不该用

Agent 不是银弹。它的价值在于“需要动态编排多个工具”的场景。

适合用的场景:

  • 企业内部知识库问答,需要检索文档并生成回答。
  • 工单处理,需要查询系统状态、调接口修改状态、生成处理记录。
  • 数据分析助手,需要根据用户自然语言生成查询、执行查询、解释结果。
  • 运维诊断助手,需要读取日志、检查指标、给出修复建议。
  • 客服辅助,需要查订单、查政策、计算赔偿金额。

不适合用的场景:

  • 简单固定的翻译、改写、摘要,用单次 Prompt 或普通 API 即可。
  • 对响应时间要求极高的接口,Agent 多轮调度会明显增加延迟。
  • 无法接受模型输出不确定性的场景,比如财务精确记账,应使用确定性代码并在完成后人工确认。
  • 工具数量少且调用路径完全固定时,用状态机或脚本比 Agent 更可靠。

这里有一个重要判断:Agent 的价值在动态编排,不在“显得聪明”。如果业务流程本身是固定的,就别硬套 Agent。

2. 环境准备:搭一套最小可复现的 AI Agent 开发环境

2.1 技术选型:先手写,再上框架

很多人在第一步就纠结“用 LangChain 还是 LangGraph,还是 Spring AI”。我的建议是分两步:

  • 学习阶段:先用原生代码手写一个 Agent 循环,代码量在 200 行以内。这样你能理解工具调用、消息回填、循环退出这些底层机制。
  • 生产阶段:再根据团队技术栈选择框架或自研编排引擎。框架能减少重复代码,但也带来了版本变动、抽象层级和排障复杂度。

选型没有绝对最优解,关键看团队技术栈:

方案适合场景学习成本风险点
原生 Python 实现理解原理、快速原型、团队规模小所有轮子要自己写
LangChain / LangGraph快速集成文档、向量库、各类工具中高版本频繁变化,API 迁移成本
Spring AIJava / Spring Boot 技术栈团队模块更新快,需核对版本
自研编排引擎业务链路复杂,企业要完全可控初期研发投入大
低代码平台非技术背景快速验证定制空间有限,难以深度集成

推荐新手走“原生代码理解逻辑 -> LangChain/LangGraph 做进阶 -> 回到工程化设计”这条路径。

2.2 开发环境清单

下面的环境不是官方定死的,而是目前最常见的搭配。落地前先确认自己团队的约定版本:

组件版本建议用途
Python3.10 或更高编写 Agent 主逻辑、工具调用脚本
Java17 或更高Spring Boot 客户端和中间服务开发
Node.js18 或更高(可选)前端或脚本工具
Docker20.10+本地启动向量库、数据库等依赖
Redis7.x会话状态、缓存、限流
向量数据库可选:Milvus、Qdrant、pgvector知识库检索
大模型服务OpenAI 兼容接口或云厂商模型服务模型接入和推理
API Key在环境变量中配置调用模型服务

这里不绑定某一家模型服务商。实际项目里你只需要一个兼容 OpenAI Chat Completions 协议的模型服务地址,很多国内云厂商都提供这类接口。关键是协议统一,后面换模型不用改业务代码。

2.3 环境变量和模型服务配置

不要把 API Key 硬编码在代码里。推荐在项目根目录维护一个.env.example文件,提交到 Git 时只保留模板,不提交真实密钥。

# .env 示例,实际密钥不要提交到代码仓库 LLM_BASE_URL=https://your-provider.example.com/v1 LLM_API_KEY=sk-your-key-here LLM_MODEL=your-model-name LLM_TEMPERATURE=0.2 LLM_MAX_TOKENS=2048 # Agent 运行参数 AGENT_MAX_ROUNDS=5 AGENT_TIMEOUT_SECONDS=60

为什么temperature要设置得比较低?Agent 在做工具调用时,我们希望模型输出尽可能确定,温度过高会让工具名称和参数出现随机性。一般工具调用场景建议设为 0 到 0.3,创意写作场景才调高。

在 Python 中读取环境变量时,可以加上缺失校验,避免启动后才发现配置错误:

import os def get_required_env(key: str) -> str: value = os.getenv(key) if not value: raise RuntimeError(f"缺少必需的环境变量: {key}") return value BASE_URL = get_required_env("LLM_BASE_URL") API_KEY = get_required_env("LLM_API_KEY") MODEL = get_required_env("LLM_MODEL") MAX_ROUNDS = int(os.getenv("AGENT_MAX_ROUNDS", "5"))

2.4 最小项目结构

先设计一个可以扩展的目录结构,后面加工具、加记忆、加接口都会比较容易:

agent_demo/ ├── .env.example ├── requirements.txt ├── README.md ├── agent/ │ ├── __init__.py │ ├── core.py # Agent 主循环 │ ├── llm.py # 模型服务客户端 │ ├── memory.py # 短期记忆和会话管理 │ └── prompt.py # 系统 Prompt ├── tools/ │ ├── __init__.py │ ├── registry.py # 工具注册表 │ ├── calculator.py # 示例工具:计算器 │ └── weather.py # 示例工具:天气查询 └── main.py # 命令行入口

这个结构的学习路径是:先看registry.py理解“工具是什么”,再看core.py理解“Agent 怎么循环”,最后看main.py理解“怎么把 Agent 跑起来”。

3. 手写一个最小 Agent:不依赖框架,先理解循环

3.1 让模型学会“选择工具”

手写 Agent 的关键是让模型输出能被程序可靠解析的“工具调用指令”。目前最常见的方案是让模型输出结构化 JSON,程序解析后执行。

一个简单的约定:

{ "tool_calls": [ { "name": "calculator", "arguments": { "expression": "(120 + 80) * 2" } } ] }

使用 JSON 而不是让模型自由输出文本,原因是解析稳定性。自由文本很难判断“模型到底想调用哪个工具、参数边界在哪里”,JSON 有明确的键值结构,出错也容易定位。

如果你接的是 OpenAI 兼容接口,可以直接使用接口原生的tools参数和tool_calls返回字段。下面为了讲清楚原理,先用手写 JSON 的方案展示循环逻辑,再在第四节用原生工具调用方案做进阶。

3.2 定义一个工具注册表

工具注册表解决两个问题:一是给模型“有哪些工具可用”的定义,二是让程序能根据工具名称找到对应执行函数。

# tools/registry.py import inspect from typing import Any, Callable, Dict TOOL_REGISTRY: Dict[str, Callable] = {} def register_tool(func: Callable) -> Callable: """注册一个可被 Agent 调用的工具函数。""" TOOL_REGISTRY[func.__name__] = func return func def get_tool_schemas() -> list[dict]: """生成 OpenAI 兼容的工具描述列表,发给模型。""" schemas = [] for name, func in TOOL_REGISTRY.items(): sig = inspect.signature(func) properties = {} required = [] for param_name, param in sig.parameters.items(): properties[param_name] = { "type": "string", "description": f"参数 {param_name}" } if param.default is inspect.Parameter.empty: required.append(param_name) schemas.append({ "type": "function", "function": { "name": name, "description": (func.__doc__ or "").strip(), "parameters": { "type": "object", "properties": properties, "required": required } } }) return schemas @register_tool def calculator(expression: str) -> str: """计算数学表达式,比如 (120 + 80) * 2。只能处理安全的算术运算。""" # 注意:生产环境不要用 eval,这里演示需要配合白名单校验 allowed = set("0123456789+-*/(). ") if not set(expression).issubset(allowed): return "错误:表达式包含非法字符" try: result = eval(expression) # 仅用于本地教学演示 return str(result) except Exception as exc: return f"计算错误: {exc}" @register_tool def query_weather(city: str) -> str: """查询指定城市的天气概况。""" # 实际项目替换为真实天气 API 调用 mock_weather = { "北京": "晴,气温 3 到 12 摄氏度", "上海": "多云转小雨,气温 10 到 16 摄氏度", "广州": "阴,气温 18 到 24 摄氏度" } return mock_weather.get(city, f"未找到 {city} 的天气数据")

这里解释两个容易踩的坑:

  • eval只在教学场景可用。生产环境执行用户传入的表达式必须做白名单校验,或者改用专用表达式解析库,否则就是代码执行漏洞。
  • 工具函数的返回值必须是字符串或能被序列化成 JSON 的对象。模型看到的是文本,结构化数据要自己转成字符串或 JSON。

3.3 核心 Agent 循环实现

Agent 主循环的本质是:把消息列表发给模型 -> 模型返回文本和工具调用 -> 如果有工具调用,程序执行并在消息列表里追加结果 -> 再发给模型,直到没有工具调用或达到上限。

# agent/core.py import json from typing import Dict, List, Optional from agent.llm import chat_completion from tools.registry import TOOL_REGISTRY, get_tool_schemas SYSTEM_PROMPT = """ 你是一个任务助手。根据用户的问题,你可以调用工具来获取信息。 工具调用规则: 1. 如果需要计算或查数据,调用工具。 2. 一次可以调用多个工具,但要确保参数完整。 3. 根据工具结果继续推理,不要重复调用同一个已成功的工具。 4. 当得到最终结论时,用中文直接回答用户,不要再输出 tool_calls。 """ def execute_tool(name: str, arguments: Dict) -> str: """根据工具名称找到函数并执行,返回字符串结果。""" func = TOOL_REGISTRY.get(name) if not func: return f"错误:未知工具 {name}" try: result = func(**arguments) if isinstance(result, (dict, list)): return json.dumps(result, ensure_ascii=False) return str(result) except TypeError as exc: return f"错误:工具参数不匹配 {exc}" except Exception as exc: return f"错误:工具执行失败 {exc}" def agent_loop(user_task: str, max_rounds: Optional[int] = None) -> Dict: max_rounds = max_rounds or 5 messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_task} ] for round_index in range(1, max_rounds + 1): print(f"[round {round_index}] 调用模型...") response = chat_completion(messages, tools=get_tool_schemas()) assistant_message = response["message"] messages.append(assistant_message) tool_calls = assistant_message.get("tool_calls", []) if not tool_calls: return { "finished": True, "rounds": round_index, "answer": assistant_message.get("content", "") } for call in tool_calls: tool_name = call["function"]["name"] raw_arguments = call["function"]["arguments"] try: arguments = json.loads(raw_arguments) if isinstance(raw_arguments, str) else raw_arguments except json.JSONDecodeError: arguments = {"raw": raw_arguments} print(f"[round {round_index}] 调用工具: {tool_name}({arguments})") result = execute_tool(tool_name, arguments) messages.append({ "role": "tool", "tool_call_id": call.get("id", f"tool_{round_index}"), "content": result }) return { "finished": False, "rounds": max_rounds, "answer": "超过最大循环次数,任务未完成。" }

这个循环需要重点理解三个点:

第一,消息列表是 Agent 的记忆载体。每一轮的工具调用和工具返回结果都追加到messages中,模型才能看到“自己刚才做了什么、结果如何”。如果不清空或截断,就会越积越长,最终可能超出模型的上下文窗口。

第二,工具结果是给模型看的,不是给用户看的。所以在追加tool消息时,内容应该是结构化、完整的,让模型能据此判断下一步。

第三,循环必须设上限。如果模型反复调用工具不结束,会让成本失控,也可能卡死在错误路径上。这里的max_rounds就是安全阀。

3.4 模型客户端封装

chat_completion是对模型服务的统一封装。以 OpenAI 兼容接口为例:

# agent/llm.py import os import requests def chat_completion(messages: list, tools: list | None = None): url = f"{os.getenv('LLM_BASE_URL', 'https://your-provider.example.com/v1')}/chat/completions" payload = { "model": os.getenv("LLM_MODEL"), "messages": messages, "temperature": float(os.getenv("LLM_TEMPERATURE", "0.2")), "max_tokens": int(os.getenv("LLM_MAX_TOKENS", "2048")), "tools": tools or [], "tool_choice": "auto" } headers = { "Authorization": f"Bearer {os.getenv('LLM_API_KEY')}", "Content-Type": "application/json" } resp = requests.post(url, json=payload, headers=headers, timeout=60) resp.raise_for_status() data = resp.json() return { "message": data["choices"][0]["message"], "usage": data.get("usage", {}) }

这里不写死某一家的地址,而是用环境变量注入基础地址。如果你的模型服务不兼容tools参数,可以暂时改用“模型输出 JSON 字符串”的方案,让模型直接输出包含tool_calls的 JSON,再用json.loads解析。

3.5 最小验证

main.py里放一个命令行入口:

# main.py import os from dotenv import load_dotenv from agent.core import agent_loop load_dotenv() if __name__ == "__main__": task = input("请输入任务:") result = agent_loop(task, max_rounds=int(os.getenv("AGENT_MAX_ROUNDS", "5"))) print("\n最终回答:") print(result["answer"])

运行:

cd agent_demo pip install python-dotenv requests python main.py

输入这样的任务:

请帮我计算 (120 + 80) * 2,然后查询北京的天气。

预期日志会显示 Agent 先调用calculator,再调用query_weather,最后输出一段结合两个工具结果的中文回答。如果模型在一次回复中没有同时生成两个工具调用,它也会在下一轮继续执行未完成的第二个工具。

可以看到,手写方案代码量不大,但它已经具备了 Agent 最核心的骨架。这个骨架在后面的框架方案里,本质上也还是这套消息循环。

4. 用 LangChain / LangGraph 实现知识库型 Agent

4.1 为什么需要框架

手写 Agent 能让你理解原理,但进入生产时还是有很多重复工作要处理:

  • 文档切分、向量化、存储和检索。
  • 多个工具定义的标准化。
  • 会话状态持久化。
  • 图状态流转,比如“先检索再回答”“失败后重试”。
  • 与外部数据库、消息队列、可观测系统的集成。

这时候使用框架能显著提高开发效率。下面以 LangChain 和 LangGraph 为例说明,代码保留常见写法的核心结构。由于这类框架版本更新频繁,落地前一定要以当前官方文档为准。

4.2 场景设定:知识库问答 Agent

很多企业做 Agent 的第一个场景就是“文档问答”:让用户用自然语言提问,Agent 从企业知识库里检索相关片段,再结合模型生成回答。

核心链路是:

  1. 把企业文档加载进来,按一定粒度切分成片段。
  2. 把片段向量化后存入向量数据库。
  3. 用户提问后,先用向量检索找到相关片段。
  4. 把片段作为上下文,交给模型生成回答。
  5. 如果知识库内容不足,Agent 可以调用外部接口补全信息。

这个场景也回应了很多人关心的“Obsidian / 本地笔记 + AI Agent 知识库”如何落地:本质都是“文档加载 -> 切分 -> 向量化 -> 检索 -> 问答”,只是不同客户端负责文档接入和交互。

4.3 文档加载和切分

from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader = TextLoader("docs/faq.txt") documents = loader.load() splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] ) chunks = splitter.split_documents(documents) print(f"文档被切分为 {len(chunks)} 个片段")

切分参数为什么重要?

  • chunk_size太小,单个片段上下文不足,检索后模型看不到完整语义。
  • chunk_size太大,向量检索精度下降,而且容易超出模型上下文限制。
  • chunk_overlap保留前后文衔接,避免语义被切断在边界处。
  • 中文场景最好在分隔符里加入中文标点,否则切分时可能把一个完整的句子拆开。

4.4 构建向量检索工具

from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import FAISS embedding = OpenAIEmbeddings( base_url=os.getenv("LLM_BASE_URL"), api_key=os.getenv("LLM_API_KEY"), model="your-embedding-model" ) vector_store = FAISS.from_documents(chunks, embedding) retriever = vector_store.as_retriever(search_kwargs={"k": 3}) @register_tool def search_knowledge(query: str) -> str: """在企业知识库中检索与 query 相关的文档片段。""" docs = retriever.invoke(query) if not docs: return "知识库中未找到相关内容。" return "\n\n---\n\n".join([doc.page_content for doc in docs])

这个工具一旦注册进 Agent,模型就会在回答前先调用search_knowledge,把检索结果作为判断依据。向量数据库选择方面,小型原型用 FAISS 或 Chroma 就够,生产环境建议使用 pgvector、Milvus、Qdrant 等支持水平扩展和高可用的方案。

4.5 用 LangGraph 控制流程

如果你希望流程更可控,可以用 LangGraph 显式定义节点和边:

from typing import TypedDict from langgraph.graph import StateGraph, END class AgentState(TypedDict): question: str context: str answer: str def retrieve_node(state: AgentState): docs = retriever.invoke(state["question"]) return {"context": "\n\n".join([d.page_content for d in docs])} def generate_node(state: AgentState): prompt = f"基于以下资料回答问题:\n\n{state['context']}\n\n问题:{state['question']}" response = chat_completion( [{"role": "user", "content": prompt}], tools=[] ) return {"answer": response["message"].get("content", "")} graph = StateGraph(AgentState) graph.add_node("retrieve", retrieve_node) graph.add_node("generate", generate_node) graph.set_entry_point("retrieve") graph.add_edge("retrieve", "generate") graph.add_edge("generate", END) app = graph.compile()

把流程拆成节点有两个好处。一是每一步可以独立测试,比如单独验证检索节点返回的内容是否相关。二是每步可以插入人工审核、限流、日志埋点和异常处理,这是生产环境必须的。

需要注意的是,这里不是“必须用 LangGraph”。如果团队已经熟悉自研编排,完全可以实现同样的状态机。框架只是减少重复代码,不能替你设计流程。

4.6 关键参数速查

框架方案里经常需要调参,整理成一张速查表会更方便:

参数默认值示例作用调大的影响调小的影响
chunk_size500每个片段的字符数上下文更完整,检索精度可能下降上下文更聚焦,容易截断语义
chunk_overlap50相邻片段重叠字符衔接更连续,存储和计算量增大可能丢失边界语义
k(检索条数)3每次返回的文档片段数上下文更充分,成本和时间上升响应更快,可能漏信息
temperature0.2采样随机性回答更多样,不稳定回答更确定,适合工具调用
max_rounds5Agent 最大循环轮数能完成更复杂任务,成本和风险上升快速终止,复杂任务可能失败

这些参数不是一次调完就固定不变。实际项目需要根据领域文档、问题类型和模型版本反复实验,最好把实验过程和结果记录到评估用例中。

5. Spring Boot 侧接入 AI Agent 客户端

5.1 Java 技术栈为什么要关心 Agent

很多企业核心系统是 Spring Boot 写的,Agent 服务往往是 Python 或独立服务,这时候 Java 侧需要一个稳定、可观测的“Agent 客户端”。职责包括:

  • 把用户请求转发给 Agent 服务。
  • 管理 HTTP 连接、超时、重试。
  • 解析统一响应结构。
  • 把 Agent 运行日志接入原有监控体系。
  • 对客户端做限流和降级。

这在架构上又叫“AI Agent 网关层”。Java 侧不直接调用大模型,而是面向内部 Agent 服务封装统一接口,这样前后端职责清晰,也方便后面替换 Agent 实现。

5.2 定义统一的接口响应结构

无论后端 Agent 服务是 Python 还是 Java 实现,建议接口响应统一为:

{ "task_id": "task_20260201_001", "status": "SUCCESS", "answer": "订单总额为 400 元,其中包含 80 元税费。", "rounds": 3, "used_tools": ["calculator", "query_policy"], "error_message": "" }

统一结构的好处是下游客户端不需要关心 Agent 内部有多少轮调用。客户端只关心状态、答案、任务ID和错误信息。

对应 Java 实体可以用一个 Record 表示:

public record AgentResponse( String taskId, String status, String answer, int rounds, List<String> usedTools, String errorMessage ) { public boolean isSuccess() { return "SUCCESS".equals(status); } }

5.3 用 RestClient 封装 HTTP 调用

Spring Boot 3.2 以后内置了RestClient,比传统的RestTemplate更简洁。示例:

@Service public class AgentClient { private final RestClient restClient; public AgentClient(RestClient.Builder builder, @Value("${agent.service.url}") String agentServiceUrl) { this.restClient = builder.baseUrl(agentServiceUrl).build(); } public AgentResponse sendTask(String userId, String question) { Map<String, Object> request = Map.of( "user_id", userId, "question", question ); try { return restClient.post() .uri("/agent/task") .header("X-User-Id", userId) .body(request) .retrieve() .body(AgentResponse.class); } catch (HttpClientErrorException ex) { throw new AgentServiceException("Agent 服务返回客户端错误", ex); } catch (HttpServerErrorException ex) { throw new AgentServiceException("Agent 服务暂时不可用", ex); } } }

这里有几个工程细节要注意:

  • agent.service.url不要写死在代码里,通过配置中心或环境变量注入。
  • 要区分客户端错误 4xx 和服务端错误 5xx,方便后续告警。
  • 不要把大模型密钥下发给前端,前端只能访问你的 Spring Boot 服务,不能直接访问 Agent 服务。

5.4 超时、重试和降级

调用远程 Agent 服务不能没有超时控制。一个 Agent 任务可能要跑 5 到 15 秒,超时时间需要比普通 HTTP 接口更长,但也不能无限等待。

spring: threads: virtual: enabled: true agent: service: url: http://agent-service:8080 connect-timeout: 3s read-timeout: 30s

在配置类中设置:

@Bean public RestClient agentRestClient(RestClient.Builder builder, @Value("${agent.service.url}") String url) { return builder .baseUrl(url) .requestFactory(getRequestFactory()) .build(); } private ClientHttpRequestFactory getRequestFactory() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(30000); return factory; }

如果选用 Spring AI 的客户端封装,思路是一样的,只是它帮你处理了聊天消息、工具定义和模型供应商协议。下面是一个基于 Spring AI 的典型写法:

@Component public class SpringAiChatAgent { private final ChatClient chatClient; public SpringAiChatAgent(ChatClient.Builder builder) { this.chatClient = builder.build(); } public String chat(String userMessage) { return chatClient.prompt() .user(userMessage) .call() .content(); } }

注意 Spring AI 的模块和 API 变化很快,不同版本对 OpenAI、通义等模型服务商的依赖与配置类不完全一致。实际接入前要核对当前版本的官方文档,不要把网上一个月前的代码直接复制进生产环境。

5.5 Java 侧的异常处理

无论使用哪种客户端,都要有一个统一异常处理,避免把底层连接池、超时异常直接抛给前端:

@RestControllerAdvice public class AgentExceptionHandler { @ExceptionHandler(AgentServiceException.class) public ResponseEntity<Map<String, Object>> handleAgentServiceException(AgentServiceException ex) { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(Map.of( "code", "AGENT_SERVICE_ERROR", "message", ex.getMessage() )); } }

6. 运行验证与结果分析

6.1 只验证“能启动”远远不够

不少 Agent 项目在开发阶段只测试了“用户提问 -> 模型回答”这一条正常路径,上线后才暴露出工具参数错误、知识库检索为空、超时、上下文爆炸等问题。

完整的验证至少覆盖四类场景:

  • 正常路径:Agent 按预期调用工具并给出正确回答。
  • 工具失败路径:工具返回错误,Agent 是否重试、换工具,还是直接给出错误结论。
  • 拒绝路径:用户提问不在允许范围内,Agent 是否安全拒绝。
  • 边界路径:问题超长、并发过高、服务超时、会话中断。

6.2 关键测试用例表格

在设计测试用例时,可以参考下面的结构:

用例类型输入示例预期行为失败表现
单工具调用计算 (120 + 80) * 2调用 calculator,返回 400模型直接口算或格式错误
多工具调用查北京的天气并计算温度换算依次调用 weather 和 calculator只完成其中一个
工具参数错误调用 calculator 时缺参数Agent 自动补齐或报参数错误工具执行异常链路断裂
知识库检索为空询问知识库中不存在的内容Agent 明确说未找到,不编造模型幻觉编造答案
超过最大轮数连续不结束的复杂任务达到 max_rounds 后强制结束任务无限执行,成本失控
远程服务超时模型服务慢响应客户端在 read-timeout 后收到错误提示请求挂起,线程池被占满

6.3 日志设计:让每一轮调用都可追踪

自研 Agent 要特别注意日志。没有日志,Agent 一旦回答错误,你根本不知道是模型决策错了、工具参数传错了,还是工具结果本身有问题。

推荐在每个关键节点输出结构化日志:

import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger("agent") logger.info("begin_task task_id=%s question=%s", task_id, user_task) logger.info("tool_call round=%s tool=%s args=%s", round_index, tool_name, arguments) logger.info("tool_result round=%s tool=%s result=%s", round_index, tool_name, result) logger.info("finish_task task_id=%s rounds=%s answer=%s", task_id, round_index, final_answer)

为什么日志要包含task_id?因为一个用户问题可能被多个微服务处理,只有全局任务 ID 才能在日志系统里把入口、模型调用、工具调用串成一条完整链路。生产环境还会配合 OpenTelemetry 等链路追踪工具,把 Agent 内部步骤和 Spring Boot 侧调用都纳入 trace。

6.4 回归测试怎么准备

Agent 输出天然带有随机性,回归测试不能只比对“字符串完全一致”。建议:

  • 准备一组固定测试集,包含正确答案关键词或结论判定规则。
  • 对工具调用顺序做断言,比如“必须先调用 search_knowledge 再生成回答”。
  • 对 JSON 解析、工具执行这类确定性逻辑做精确断言。
  • 使用较低 temperature(0 到 0.2)降低随机性。
  • 定期人工抽检新版本模型或框架升级后的输出质量。

7. 常见问题排查

7.1 排查顺序

Agent 问题比普通接口更难排查,因为它涉及模型、工具、框架和网络四层。建议按下面顺序定位:

  1. 确认输入和使用场景是否符合预期。
  2. 确认模型服务和 API Key 是否可用。
  3. 确认消息列表和上下文是否被正确传递。
  4. 确认工具调用参数是否完整、解析是否正确。
  5. 确认工具执行结果是否被正确回填给模型。
  6. 查看日志中的每一轮tool_calltool_result
  7. 确认是否触发了最大轮数或超时限制。
  8. 检查框架版本和依赖是否与代码示例一致。

7.2 高频问题与处理方案

问题现象常见原因检查方式处理建议
调用模型接口报 401 或 403API Key 无效或未注入环境变量检查环境变量、服务商控制台重新生成密钥,确保 Secret 不泄露
Agent 不调用工具,直接回答系统 Prompt 未明确工具规则,或tools参数未传入打印请求 payload 中的 tools在 Prompt 中说明“需要计算或查数据时必须调用工具”
工具调用参数解析失败模型输出 JSON 不合法或字段缺失打印raw_arguments原始字符串增加 JSON 解析兜底,失败时告诉模型重新生成
工具执行报参数不匹配工具函数参数名或类型与模型生成不一致对比 schema 定义和实际请求参数用清晰的工具函数名和参数描述,必要时做参数补全
上下文长度超限多轮工具调用导致消息列表过长统计请求 token 用量启用摘要压缩,或只保留最近 N 轮工具结果
Agent 陷入死循环模型反复调用同一个工具观察日志中的 round 分布设置 max_rounds,检测重复工具调用并主动终止
知识库检索结果不相关chunk 切分过大或 embedding 模型不合适打印检索到的 document 内容调整 chunk_size,换向量模型,或增加重排模型
Spring Boot 读不到配置环境变量名或 yaml 层级写错查看启动日志配置绑定信息使用@ConfigurationProperties校验配置映射
Agent 任务超时模型响应慢或工具执行慢看模型接口耗时分位值提高 read-timeout,改用异步任务,增加限流

7.3 一个典型日志排查案例

假设用户问“北京到上海的距离是多少”,Agent 却回答说“北京天气晴”。日志中可能出现:

[round 1] 调用工具: query_weather({"city": "北京"}) [round 1] 工具结果: 晴,气温 3 到 12 摄氏度 [round 2] 调用工具: query_weather({"city": "上海"}) [round 2] 工具结果: 多云转小雨,气温 10 到 16 摄氏度 [round 3] 没有工具调用,模型直接回答

从日志看,Agent 调用了两次天气工具,说明模型把“距离”问题错误拆解成了“查询天气”。此时要检查:

  • 工具注册表里是否真的存在“距离查询”工具。如果根本没有这个工具,模型只能退而求其次调用不相关工具。
  • 工具名称和描述是否足够语义化。query_weather这个名字在“距离”问题中显然不能吸引模型正确选择。
  • 系统 Prompt 是否明确“如果工具不相关,不要强行调用”。

修复方向不是盲目调 Prompt,而是先补工具,再调整工具描述,最后再考虑用规则拦截明显不相关的工具调用。

8. 从 Demo 到企业级:工程化改造清单

8.1 配置外置与密钥管理

开发时把 API Key 写在.env里没问题,但生产环境必须接入独立的密钥管理平台或云厂商密钥服务。要把以下配置全部外置化:

  • 模型服务地址、模型名称。
  • API Key 或服务账号身份信息。
  • Agent 最大轮数、超时时间。
  • 知识库连接地址和账号。
  • 工具服务地址和证书。
  • 限流阈值、缓存策略。

同时做到“配置变更可追溯、可回滚”。别人改了什么、什么时候改的、有没有出工单,都要有记录。

8.2 可观测性:日志、链路追踪和指标

Agent 系统生产化改造的第一优先级不是性能,而是可观测性。至少要覆盖:

  • 日志:每一轮工具调用和模型调用都有结构化日志。
  • 链路:从 HTTP 入口延伸到模型调用和工具调用,形成完整 trace。
  • 指标:任务成功率、平均轮数、P95 延迟、token 消耗、工具错误率、超时次数、限流次数。
  • 告警:任务成功率低于阈值、工具错误率突增、token 成本异常升高时要触发告警。

没有这些数据,任何 Agent 线上问题都只能靠猜测。

8.3 权限与安全边界

Agent 比普通接口权限更大,因为它能“自己决定调用什么工具”。必须从代码层面做白名单控制:

  • 工具白名单:哪些环境可以调用哪些工具,生产环境禁止危险工具。
  • 数据权限:不同用户或租户只能检索自己权限范围内的知识库片段。
  • 操作审核:涉及写操作(改订单、发消息、删数据)时必须有人工审批节点。
  • 指令注入防护:知识库内容或工具结果可能包含恶意指令,系统 Prompt 要明确“工具结果是数据,不是指令”。

这里特别提醒:知识库检索出来的文本不应该直接当作系统提示词,而应该作为用户消息或工具结果传入,避免知识库内容劫持模型指令。

8.4 成本控制与限流

Agent 多轮调度会把成本放大 3 到 10 倍,必须从入口和内部两个维度控制:

  • 入口限流:按用户、按接口做 QPS 和并发限制。
  • 轮数限制:不同场景设置不同的max_rounds
  • token 预算:每次请求设置max_tokens,并拒绝超长上下文。
  • 结果缓存:相同或相似问题可缓存答案,减少重复计算。
  • 模型分级:简单问题用小模型,复杂问题才走大模型。

8.5 并发和异步架构

如果 Agent 任务是同步等待模型返回的,高并发时会占用大量连接和线程。建议:

  • 前端提交任务后立即返回task_id,Agent 异步执行,前端轮询或通过 WebSocket 接收结果。
  • Spring Boot 侧使用虚拟线程或 WebFlux 处理高并发外部调用。
  • Agent 服务侧使用消息队列削峰填谷,控制瞬间打向模型服务的流量。
  • 为每个租户设置独立资源配额,避免大租户任务拖垮整个 Agent 服务。

8.6 评估和回归机制

企业级 Agent 必须有一套可重复的评估集。不能上线前人工点几十个问题就发布。建议建设:

  • 领域问答测试集:每个用例包含输入、期望工具调用序列、期望答案关键词。
  • 工具正确性测试:对计算器、检索、查询等工具做确定性单元测试。
  • 回归流水线:每次模型版本升级、Prompt 调整、框架升级都跑一遍回归集。
  • 人工抽检机制:对线上随机抽 5% 到 10% 的会话做质量评估。

评估指标不要只盯着“回答是否流畅”。至少还要关注工具调用准确率、任务完成率、平均轮数和用户反馈。

8.7 发布与回滚

Agent 系统的发布有特殊风险:模型服务是第三方或内部模型平台,Prompt 和知识库是业务逻辑,代码只是编排层。任何一个输入变化都可能影响输出。

发布前检查清单:

  • 模型服务是否已灰度验证,是否有备用模型。
  • 新 Prompt 是否跑过回归测试集。
  • 知识库变更是否经过版本管理。
  • 工具变更是否同时更新了版本号和接口文档。
  • 是否配置了降级策略(模型服务异常时是否返回固定兜底文案)。
  • 是否保留了上一个版本的核心配置和 Prompt 快照。

回滚不是只有代码回滚。Prompt、知识库、工具列表、模型版本都要支持回滚,否则出现问题后无法快速恢复。

结尾:从手写循环到生产体系

AI Agent 开发最核心的能力是理解“模型输出不确定性”和“工程确定性”之间的结合点。模型负责生成决策,但代码负责约束、验证、记录和兜底。这篇文章从手写 Agent 循环讲到了 LangChain / LangGraph 知识库 Agent,再到 Spring Boot 客户端集成和上线前的工程化改造,整条链路的关键判断只有一个:不要让大模型直接暴露在业务边界上,而是把工具调用、权限、日志、限流和回滚都变成可管理的工程代码。

对刚入门的人,建议先把你自己的 Agent 循环跑通,然后自己加一个数据库查询工具,再做知识库检索,最后接入 Spring Boot 接口。对已经在做生产 Agent 的人,建议优先补齐可观测性和评估集,没有这两项,后续所有优化都会失去方向。下一步可以考虑深入研究 ReAct 模式、多智能体协作、记忆持久化和评估系统设计,这些方向都是在本文所述骨架上的自然延伸。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 1:29:54

FanControl + Rainmeter 联动:10 分钟搭建实时硬件监控桌面

FanControl Rainmeter 联动&#xff1a;10 分钟搭建实时硬件监控桌面 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trendin…

作者头像 李华
网站建设 2026/9/7 1:27:59

赛尔号TOH地狱无表打法:工程型机制建模通关法

打赛尔号高难副本&#xff0c;最让人难受的往往不是打不过&#xff0c;而是你明明照着攻略把精灵、技能、顺序全部配好&#xff0c;进去之后还是翻车。尤其是 TOH-地狱这种名字里带“地狱”的挑战&#xff0c;很多玩家第一反应是去翻攻略表、出招表、配置表&#xff0c;结果连续…

作者头像 李华
网站建设 2026/9/7 1:27:17

嘉立创EDA与AI设计入口:硬件创新的隐性加速器

嘉立创要上市的消息在硬件圈传开时&#xff0c;我所在的几个工程师群都炸了。有人半开玩笑地发了一句&#xff1a;“天天白嫖的免费EDA&#xff0c;竟然要上市了&#xff1f;”这句话其实精准戳中了很多人的共同记忆——从大学画第一块板子开始就用立创EDA&#xff0c;工作后打…

作者头像 李华
网站建设 2026/9/7 1:26:30

英文访谈视频中文字幕制作实战:从语音转写到FFmpeg压制

拿到一段英文访谈视频时&#xff0c;如果希望中文观众能直接看懂&#xff0c;最常用的方案是制作一条时间轴准确、文本通顺的中文字幕。字幕制作并不是“把英文翻译成中文再保存成 txt”这么简单&#xff0c;它涉及语音转写、时间轴校对、翻译、字幕样式、编码处理、封装与压制…

作者头像 李华
网站建设 2026/9/7 1:24:55

Java面试常问问题:如何清晰回答异常处理机制

《荀子大略》有言&#xff1a;"先事虑事&#xff0c;先患虑患。"放在Java面试的异常处理话题上&#xff0c;这句话再贴切不过。面试官抛出"讲讲Java的异常处理机制"&#xff0c;绝非想听你背诵try-catch-finally的语法格式。他真正在意的&#xff0c;是你能…

作者头像 李华
网站建设 2026/9/7 1:24:50

机器人行业不缺ROS2工程师,缺的是能搞定底层的嵌入式人才

国内机器人赛道这两年肉眼可见地热起来了&#xff0c;从工业机械臂到服务机器人&#xff0c;再到人形机器人&#xff0c;资本、政策、跨界玩家都在往里冲。但作为在这个行业里待了快十年的嵌入式老兵&#xff0c;我这两年最直观的感受是&#xff1a;行业嘴上喊着缺ROS2工程师&a…

作者头像 李华