问数Agent基础设施搭建实战:模型、记忆与工具链路全解析

📅 发布时间:2026/9/11 4:40:52
问数Agent基础设施搭建实战:模型、记忆与工具链路全解析
上一篇文章我们把问数项目智能体的整体架构聊透了这篇开始正式动工。问数项目智能体的核心目标很简单让业务同学用自然语言提问系统自动完成取数、分析、出结论这一整条链路。比如业务人员问一句“上个月华东区销售额环比增长了多少主要受哪个品类影响”智能体要能拆出时间范围、地域维度、指标口径去数据库里取出数据再结合业务知识库给出结论。这个项目里基础设施不是锦上添花而是决定后续开发能不能跑起来的关键。模型接入、工具调用、记忆存储、可观测性全部要在地基阶段定好。我这次以 Java Spring Boot 3 Spring AI 为主线路来搭这套方案在企业内部落地时更容易被现有团队接受维护成本也低。如果你团队是 Python 生态后面我也会提到对应的替代方案。基础设施搭建最忌讳一上来就写一大堆业务代码结果连一次完整的模型调用都没跑通。正确顺序应该是先把“模型能回话、工具能注册、记忆能读写、日志能看到”这条最小链路打通再往里面填业务逻辑。这篇文章会把这四个部分全部拆开讲包括选型原因、具体配置、代码骨架和实际踩过的坑适合正在规划 Agent 项目、或已经写完 POC 准备正经搭工程的开发者参考。1. 项目全景与基础设施清单1.1 问数Agent到底在解决什么问题先明确一下业务场景。问数项目的本质是“自然语言到数据的翻译器”。传统方式下业务方取数据要走工单、等排期、沟通口径一个简单的查询可能拖上一两天。而 Agent 要做的就是把这个过程压缩到分钟级。但它面临的复杂度比普通问答系统高很多因为每一步都可能出错意图识别错了、SQL 生成错了、数据口径不对、结论解释跑偏任何一个环节都会让用户对系统失去信任。所以我搭基础设施时先定了一个原则所有模块都要能被观测、能被替换、能被单独调试。这句话听上去很空落实下来就是几件事。模型层不能写死在某个供应商 SDK 里要能随时切换模型工具层不能散落在业务代码中要能统一注册和鉴权记忆层不能只存个聊天记录要能支撑上下文检索和口径沉淀日志必须带链路 ID出问题时能还原完整对话过程。这个原则贯穿整个基础设施设计后面每一个模块都是围绕它展开的。1.2 基础设施需要覆盖的四件事一个问数 Agent 要能稳定工作基础设施需要提供四类能力。第一是模型通信通道。Agent 的所有决策都来自大模型基础设施要保证模型调用稳定、超时可控、失败有重试、返回可以被解析。这块最容易出问题的地方是模型返回格式不稳定后面我会专门讲结构化输出。第二是 Agent 运行时。这是一段循环逻辑把用户问题交给模型、模型决定调用哪个工具、工具返回结果、把结果交还给模型继续推理直到模型认为信息足够、生成最终答案。这个循环是 Agent 的心脏基础设施要把它做得足够通用不能把业务逻辑写死在循环里。第三是记忆与知识存储。对话过程中的上下文要存住历史查询记录和指标口径要能检索。问数项目里这部分尤其重要因为业务方经常会问“上次那张表里华东区的数据是多少”没有记忆系统这种问题完全没法答。第四是工具与数据通路。Agent 需要真实的数据访问能力SQL 执行、接口调用、文件解析都算工具。工具链路的安全性是问数项目的命门数据库账号权限、查询白名单、结果脱敏都要在这一层解决。这四件事不是平行的而是一层层叠加的关系。模型通道在最底下Agent 运行时跑在模型之上工具通路为运行时提供能力记忆系统贯穿始终。我搭基础设施的顺序也按照这个依赖关系来先模型、再运行时、再记忆和工具最后加可观测性。1.3 技术选型与最小依赖清单选型方面我直接讲结论。主线方案是 Java 17 Spring Boot 3.2 Spring AI向量存储用 pgvector关系库用 MySQL 8编排层面用 Spring AI 自带的工具调用机制同时预留 MCP 协议扩展。这套组合的好处是对接了 Spring 生态企业里做数据平台或中台的团队上手很快而且 Spring AI 对 OpenAI 协议、通义千问、智谱等模型做了统一抽象切换模型时不需要改业务代码。如果你团队是 Python 技术栈替代方案是 FastAPI LangGraph Chroma 或 pgvector核心思路完全一致只是 API 风格和生态不同。我代码示例会以 Java 为主但设计思想两种语言通用。最小依赖清单如下模块技术选型用途工程骨架Spring Boot 3.2 Maven 多模块代码结构划分模型接入Spring AI spring-ai-openai统一模型调用入口Agent 运行时Spring AI ChatClient ToolCallingManager工具调用循环关系库MySQL 8会话记录、指标口径、审计日志向量库PostgreSQL pgvector知识库向量检索缓存Redis可选热点会话与限流可观测性Logback SLF4J Actuator链路日志与健康检查我建议硬性依赖控制在三四个以内能不上 Redis 就先不上能让 MySQL 抗住的就用 MySQL 抗住。基础设施阶段每多一个中间件后续排查问题就多一分复杂度。先把最简单的架构跑通后面按需演进。2. 工程骨架与模块边界2.1 多模块结构设计Agent 项目最怕代码堆在一个服务里业务逻辑、提示词、工具调用、数据库访问全部耦在一起改一处坏一片。我在项目里用的是 Maven 多模块按依赖方向从内到外分了四层。agent-core 放核心领域模型和 Agent 运行时逻辑不依赖任何框架只定义接口和实体。agent-infrastructure 放模型接入的适配、数据库访问、向量检索这些技术细节。agent-tools 放具体的工具实现比如执行 SQL、查询元数据、调用 BI 接口。agent-interfaces 是应用入口负责接收请求、组装参数、返回结果。这个依赖方向是单向的interfaces 依赖 tools、infrastructure 和 coretools 依赖 coreinfrastructure 只依赖 core。core 是最稳定的内核任何技术选型换掉core 的业务含义都不会变。实际落地中这个结构的收益非常大比如我把模型从 OpenAI 兼容接口切到国产模型时只动了 infrastructure 层的实现core 和 tools 完全没改。2.2 统一返回模型与异常体系Agent 接口的返回结构和传统接口不太一样。一次请求可能耗时几十秒中间要调用模型、执行工具、再调用模型任何一个子环节失败都不能简单返回 500。我设计了一套分级返回模型顶层结构固定为 traceId、code、message、datadata 内部再细化成 AgentResponse包含完整对话记录、引用过的工具列表、Token 消耗和耗时。异常体系也要提前定好。我把异常分成几类模型调用异常 ModelException、工具执行异常 ToolException、参数校验异常 ParamException、外部服务异常 ExternalException。每类异常都有明确的错误码和用户可读信息前端拿到后可以直接展示。这套体系建起来很快但能省掉后面大量联调扯皮的时间。2.3 配置文件里的“地基参数”Spring Boot 的配置文件看着简单实际上有不少参数直接决定了 Agent 能否在生产环境稳定运行。我用的模板大致是下面这样关键参数我加了注释。spring: application: name: question-analyzer-agent ai: openai: base-url: ${LLM_BASE_URL} api-key: ${LLM_API_KEY} chat: options: model: ${LLM_MODEL:gpt-4o-mini} temperature: 0.1 datasource: url: jdbc:mysql://${DB_HOST}:${DB_PORT}/agent_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: ${DB_USER} password: ${DB_PASSWORD} mcp: client: enabled: true server: port: 8080 agent: max-loop-count: 5 sql: timeout-seconds: 10 memory: max-history-messages: 20 observability: token-stat-enabled: truetemperature 设置为 0.1 是很关键的。问数场景里我们要的是确定性不是创造性。如果 temperature 设太高同一个问题两次查询可能生成不同的 SQL结果不一致会让用户彻底失去信任。max-loop-count 设为 5 是给 Agent 循环加的保险丝防止模型陷入工具反复调用的死循环。这些参数都属于“平时不起眼、出事就是大事”的类型建议一上来就配好。3. 模型接入层详解3.1 为什么要单独封装一层模型接入很多初学者直接在自己代码里 new 一个 OpenAI 客户端然后到处调用这是基础设施建设里最容易埋雷的做法。问数项目上线后要应对的是多租户、多模型、不同量级并发的场景如果模型调用散落在各个业务代码里限流、降级、重试、成本核算都没法统一做。我把模型接入收敛成两个核心接口。一个是 ChatClient封装“传入消息列表返回回复”这个动作另一个是 EmbeddingClient负责把文本转成向量。业务代码只依赖这两个接口不需要关心底层是 OpenAI 还是国产模型。这样设计之后做模型灰度、请求限流、故障切换都变得非常轻松只需要在代理层加规则就好。3.2 基于 Spring AI 的统一模型接入Spring AI 把不同模型的 API 差异基本抹平了接入 OpenAI 兼容协议时只需要配置 base-url 和 api-key。国内很多模型的接口现在都兼容 OpenAI 格式所以这个配置可以直接复用。项目里我建了一个 ModelRouter 类用最简单的方式做多模型路由。Component public class ModelRouter { private final MapString, ChatClient chatClients; public ModelRouter(ListChatClient clients) { this.chatClients clients.stream() .collect(Collectors.toMap(this::extractModelName, Function.identity())); } public ChatClient getClient(String modelName) { return chatClients.getOrDefault(modelName, chatClients.get(default)); } private String extractModelName(ChatClient client) { // 从配置里读取 model 名称并注册到 map return client.getDefaultModel(); } }这里有个容易踩的坑Spring AI 的 ChatClient 默认是无状态的每次调用都要拼完整的消息列表。很多新手把聊天记录存在本地变量里一次请求结束就丢了。正确做法是完整消息列表始终从记忆层读取ChatClient 只负责执行单次推理。会话记忆的设计我放到下一章详细讲。3.3 结构化输出让模型永远返回合法 JSON问数 Agent 里模型不只是聊天它要做任务规划、生成 SQL、选择工具这些都需要结构化的输出。如果让模型自由发挥文本后面解析必然出问题。建议直接用 Spring AI 的 StructuredOutputConverter底层通过 JSON Schema 约束模型输出格式。我给模型定义了一个统一的行为协议所有决策类输出都遵循这个结构thought 字段放思考过程action 字段是工具名actionInput 是工具参数。这样 Agent 运行时解析一次就能拿到全部信息不需要针对不同模型写不同解析器。public record AgentDecision( String thought, String action, String actionInput ) {}使用 StructuredOutputConverter 时有个重要细节temperature 必须设低最好不超过 0.2否则模型可能返回格式正确但内容随机的内容。另外 JSON Schema 的描述字段写清楚也很重要模型会把描述当成指令去理解描述越准确输出越稳定。我甚至在描述里示例了“action 可选值为 QUERY_SQL、GET_TABLE_SCHEMA、SEARCH_KNOWLEDGE、ANSWER”效果比只写一句“按格式返回”好得多。3.4 Token 预算与上下文裁剪模型上下文长度是有限的但业务对话会不断累积。我在基础设施层直接做了上下文预算管理原则很简单把珍贵的上下文窗口留给当前任务历史信息摘要化。每个会话最多保留 20 条完整记录超过之后对更早的内容做摘要。这跟在办公室记笔记一个道理重要信息完整记录过期细节压缩成结论。public class ContextWindowManager { private static final int MAX_FULL_MESSAGES 20; public ListMessage buildMessages(ConversationSession session, String userQuestion) { ListMessage messages new ArrayList(); if (session.messageList().size() MAX_FULL_MESSAGES) { messages.add(new SystemMessage(session.getSummary())); ListMessage recent session.messageList() .subList(session.messageList().size() - MAX_FULL_MESSAGES, session.messageList().size()); messages.addAll(recent); } else { messages.addAll(session.messageList()); } messages.add(new UserMessage(userQuestion)); return messages; } }Token 成本也要在这一层就能看到。我在模型代理里埋了计数点每次调用记录 promptTokens 和 completionTokens按模型单价折算成本。如果发现某个用户或某个工具的调用成本异常高可以快速定位。4. 记忆层与向量检索设计4.1 会话记忆的三层结构问数 Agent 的记忆不能只做一个表存聊天记录我把记忆分成三层短期会话记忆、长期项目记忆、业务口径记忆。短期会话记忆保存当前对话上下文存 MySQL 的 conversation_message 表字段包括会话 ID、消息角色、内容、Token 数、创建时间。长期项目记忆保存用户在过去时间内的查询习惯和常用条件比如某用户常看华东区数据、关心毛利指标。业务口径记忆是问数项目最特别的部分它保存指标定义、表字段说明、口径规则查询前先检索这段记忆能显著降低 SQL 生成错误率。4.2 数据库表结构与关键 SQL对话消息表我建得比较克制不搞过度设计核心就是下面这个结构。CREATE TABLE conversation_message ( id BIGINT AUTO_INCREMENT PRIMARY KEY, session_id VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL, content TEXT NOT NULL, token_count INT DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_session_time (session_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;查询时按 session_id 和 created_at 排序用 limit 控制返回条数。这里有个细节不要一次把整个会话所有消息都查出来给模型因为大多数历史消息的价值密度很低反而会稀释模型对当前问题的注意力。我通常每轮只取最近十条消息最早的那条用户问题必须保留那是对话的起点。4.3 向量库选型与知识分片策略问数项目的知识库主要存三类内容指标口径文档、表结构说明、常见问答对。这些内容天然适合做向量检索用户问“毛利率怎么算”通过向量召回“毛利率 毛利 / 收入”这条口径说明再把它拼进系统提示词SQL 生成时就能用上。向量库我选的是 pgvector原因很朴素项目本来就要用 PostgreSQL 存业务数据再装一个 pgvector 扩展就行不需要额外维护一套向量数据库。几千条知识文档用 pgvector 完全够用配合 HNSW 索引召回延迟在几十毫秒以内。只有当数据量到百万级、并发又高的时候才需要考虑独立的向量库。CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE knowledge_doc ( id BIGSERIAL PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, source VARCHAR(64), embedding vector(1536), created_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX ON knowledge_doc USING hnsw (embedding vector_cosine_ops);知识分片是决定检索效果的关键。我踩过几次坑之后总结出的经验是按语义完整度切分而不是按固定字数。比如指标口径文档每一段必须是一个完整的指标说明包含名称、公式、取数范围不能把一个指标拆到两个分片里。分片时我还保留了标题字段检索答案时把标题拼在内容前模型能更好理解上下文。切分粒度上中文字数在 300 到 500 字之间效果比较稳太短了缺少语义太长了检索精度下降。5. Agent 运行时与工具调用实现5.1 Agent 运行循环的结构化设计Agent 运行时的核心是一段循环逻辑但它绝不是简单的“while true 调模型”。我把它设计成四步循环推理、规划、执行、观察。模型每次输出一个 AgentDecision运行时代理解后去调用对应的工具再把工具结果封装成 Observation 消息放回上下文然后进入下一轮推理。这个循环一直持续到模型认为信息足够输出最终答案。循环一定要设置最大轮数我设在五轮以内。这个限制是保护系统不失控的关键因为任何复杂查询最多用到三次工具调用如果模型陷入反复调用大概率是提示词或工具设计有问题停下来比无限转圈要好。为了观察每轮循环发生了什么我记录了每一步的完整日志包括模型思考内容、选了哪个工具、参数是什么、工具返回了多久这些日志在排查问题时价值极大。5.2 MCP 协议与工具注册机制工具层我用 MCPModel Context Protocol来统一管理。MCP 的作用是定义一套标准协议让模型能发现工具、理解工具、调用工具。在这个设计里每个工具就是一个独立服务通过 MCP 协议向 Agent 注册。为什么要用 MCP 而不是自己撸一套工具调用接口因为 MCP 天然解决了工具能力和 Agent 的解耦问题。问数项目会持续增加新的数据源和工具比如接一个内部 BI 系统、接一个报表导出服务如果每个工具都自定义调用协议后续扩展的维护成本会指数级上升。MCP 好了之后新工具只需实现标准协议Agent 运行时不需要改动。Java 生态里 Spring AI 对 MCP 客户端做了集成配置好后可以用 Tool 注解直接注册工具方法。下面是一个查询表结构的工具示例。Component public class QueryDataTools { private final JdbcTemplate jdbcTemplate; public QueryDataTools(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Tool(description 查询数据库中指定表的字段元数据输入表名返回字段名、类型和注释) public String getTableSchema(String tableName) { // 只查询 information_schema不执行真实业务查询 String sql SELECT column_name, data_type, column_comment FROM information_schema.columns WHERE table_schema DATABASE() AND table_name ? ; ListMapString, Object columns jdbcTemplate.queryForList(sql, tableName); return JSONUtil.toJsonStr(columns); } }工具方法的描述信息一定要写得非常准确因为大模型靠描述决定什么时候调用、传什么参数。描述里要写清楚任务边界和参数约束。比如这个查询表结构的工具要在描述里告诉模型“只查询元数据不会执行业务查询”避免模型在拿不准时反复用这个工具探查无关内容。5.3 数据库安全通路只读账号是底线问数项目最具风险的地方是让模型生成 SQL 并执行。模型会写 SQL 不代表它能写好 SQL更不代表它不会写出灾难性的 SQL。所以我不让模型直连业务主库而是给它一个独立的只读从库账号。这个账号权限要收紧只能 SELECT、不能 UPDATE 和 DELETE连接池要设置最大连接数和单条查询超时。Configuration public class DataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource.agent) public DataSource agentDataSource() { return DataSourceBuilder.create().build(); } Bean public JdbcTemplate agentJdbcTemplate(Qualifier(agentDataSource) DataSource ds) { JdbcTemplate jdbcTemplate new JdbcTemplate(ds); jdbcTemplate.setQueryTimeout(10); jdbcTemplate.setMaxRows(500); return jdbcTemplate; } }setQueryTimeout(10) 和 setMaxRows(500) 这两行是安全生命线。查询超时防止慢 SQL 拖垮数据库最大行数防止模型写出的笛卡尔积或全表查询返回巨大数据量。虽然只读账号不能写数据但它可以发起全表扫描照样能把数据库 IO 打满。所以基础设施层面还必须做数据源限流我用的 HikariCP 连接池里把 maximumPoolSize 调到了 10线上再配合网关限流。SQL 白名单策略也值得考虑。我建了一个 query_policy 表按表名配置允许查询的范围和返回行数上限。模型生成的 SQL 在真实执行前会先做一个词法解析检查涉及的表是否在白名单内并识别明显的全表扫描模式。这块规则写得粗一点没事目的是挡住绝大多数低级错误真正复杂的管控靠只读账号和超时兜底。6. 可观测性与配置安全6.1 链路日志Agent 排障的第一利器联调阶段最痛苦的事情是用户问了一个问题系统返回了错误答案但不知道是哪一步错了。是模型理解错了是 SQL 写错了还是工具调用失败了基础设施里必须把这个过程完整记录。我给每次请求生成一个 traceId从接口入口开始贯穿到模型调用、工具执行、数据库访问所有日志都打上这个 ID。日志内容包括每一轮推理的模型输入和输出、工具名称和参数、工具返回耗时、上下文窗口的 Token 量。排查问题时直接按 traceId 搜索整个决策链条一目了然。Component public class AgentTracer { private static final Logger log LoggerFactory.getLogger(AgentTracer.class); public void traceDecision(String traceId, AgentDecision decision) { log.info(traceId{}, thought{}, action{}, actionInput{}, traceId, decision.thought(), decision.action(), decision.actionInput()); } public void traceToolInvoke(String traceId, String toolName, long costMs, boolean success) { log.info(traceId{}, tool{}, costMs{}, success{}, traceId, toolName, costMs, success); } }实际用下来链路日志对系统优化的帮助比任何监控面板都大。比如发现某类问题的 SQL 查询特别慢就能从日志里看到是模型选了低效的查询条件发现某个工具的调用频率异常高就说明提示词的工具选择策略需要调整。没有这些日志所有优化都是拍脑袋。6.2 Token 消耗与成本核算大模型项目的成本如果不做记录上线后一定会被账单吓到。我在基础设施里加了一个简单的成本计量模块每次模型调用完成后记录模型名、输入 Token、输出 Token、预估费用。数据落到每日汇总表里按用户、按会话、按工具三个维度统计。有一个容易被忽略的点工具返回给模型的结果也是 Token 成本的大头。比如执行 SQL 返回了 500 行数据这 500 行要全部拼进上下文让模型总结按每千 Token 计费一次复杂查询的上下文就可能上万 Token。我在工具层做了结果治理SQL 查询结果先做聚合和截断再交还模型能减少一半以上的 Token 消耗。这不是偷工减料模型看到 500 行原始数据不一定比看到一行 SUM 结果总结得更好。6.3 敏感配置管理与审计模型 API Key、数据库密码这些配置不能硬编码在代码里更不能提交到 Git。我用的是环境变量加配置中心的方式本地开发用 Spring Boot 的 profile 区分线上从配置中心拉取。Spring 的 ConfigurationProperties 会把配置映射到类的字段上开发时用本地环境变量部署到 Kubernetes 时映射成 Secret统一管理。数据库查询操作要做审计。所有 Agent 执行过的 SQL 语句都要记录到审计表包含用户、会话 ID、SQL 内容、执行耗时、影响行数。这不是为了追究责任人而是为了复盘系统行为。当用户投诉“这个数据不对”的时候能快速查到这个数据是哪个问题、用什么 SQL、什么条件查出来的。审计记录和链路日志配合使用能让绝大部分问题在三十分钟内定位。CREATE TABLE query_audit_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, session_id VARCHAR(64) NOT NULL, user_id VARCHAR(64) NOT NULL, sql_text TEXT NOT NULL, cost_ms BIGINT, row_count INT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;7. 常见问题与排查技巧实录7.1 常见问题速查表基础设施搭建和联调阶段我整理了最常遇到的几个问题放在一个表格里方便对照排查。异常现象可能原因排查方法解决方案模型返回乱码或非 JSON上下文里混入了过长工具结果查看链路日志确认输入消息对工具结果做截断与聚合SQL 生成总是选错表元数据信息不足或描述不清晰检查送进上下文的 schema 信息补充表注释和字段枚举值说明工具调用超时数据库连接池耗尽或查询慢查看连接池指标和慢查询日志增加连接数、优化 SQL 生成Agent 陷入工具循环工具描述误导模型反复调用查看决策日志确认意图收敛工具描述、调整提示词输出结果前后不一致temperature 过高或未绑口径复测多次相同问题temperature 调低绑定口径上下文越长响应越慢历史消息全量塞入模型检查上下文窗口管理逻辑启用摘要和历史裁剪机制多租户数据串了SQL 参数拼接缺失租户过滤条件审计日志比对查询条件强制注入租户 ID 过滤这些问题的共同点在于大部分不是代码逻辑错误而是模型行为和上下文管理的问题。排查时不要只盯报错信息要看整个决策链路里模型看到了什么、为什么这么判断。这也是我一直强调链路日志重要的原因。7.2 先用 Mock 模型跑通再上真实模型基础设施刚搭好时别急着接真实的大模型。我用了一个简单策略先写一个 FakeChatClient根据输入返回预设的决策结果比如固定调用某个工具、传固定参数。这样做有几点好处。第一可以验证工具注册和执行链路是否正确比如工具描述能不能被正确加载、参数解析有没有问题。第二可以测试 Agent 循环的边界条件比如工具返回格式错误时系统会不会崩溃、循环超过最大轮数时会不会有明确报错。第三方便写自动化测试每次提交代码后跑一遍确保基础设施没有被改坏。Mock 模型的实现非常简单就是一个实现 ChatClient 接口的类根据关键词返回预设内容。我建议把这个 Mock 模型作为开发测试的标准配置放在测试目录里等所有基础设施都稳定了再切换到真实模型联调。这一步省了我大量排查“到底是模型不行还是代码不行”的时间。7.3 工具返回必须结构化而且要有预算意识我发现很多 Agent 项目的工具返回是全凭模型自由发挥的文本这会导致下游解析很不稳定。我的原则是所有工具返回统一为 JSON 字符串并且顶层结构固定。比如 SQL 查询工具无论查出来多少行固定返回一个包含 columns 和 rows 的 JSON模型拿到手就知道怎么用。工具返回里的关键字段都要有明确单位比如金额字段带上“单位元”日期字段统一“YYYY-MM-DD”格式避免模型解释时出错。工具结果进入上下文前要计算 Token 消耗。我给工具输出加了一个预算上限比如单个工具结果超过 1500 Token 就必须做裁剪。裁剪策略是先保留全表字段名和头部数据样本再进行聚合转换。把 500 行原始数据转成按维度分组的汇总表完全能支撑绝大多数业务分析问题但 Token 成本能省下五倍以上。7.4 数据分析 Agent 独有的性能调优点问数 Agent 的延迟大头通常不在模型推理而在 SQL 查询环节。模型推理虽然要几百毫秒到几秒但 SQL 查询动辄就是几秒甚至几十秒。基础设施里针对这个做了两层优化。第一层是元数据预加载。表结构和字段注释这类信息不需要每轮实时查库启动时缓存到本地定期刷新。第二层是结果缓存。完全相同的查询在短时间内重复出现时直接从缓存取结果不再查库。这两层优化能把常见查询的端到端延迟从十秒级降到了两三秒体感提升非常明显。还有一个容易被忽略的地方数据库连接池大小要和并发请求数匹配。线程池里的 Agent 任务在等待 SQL 结果时会占用线程如果连接池太小线程会集体等待连接释放整个服务的吞吐量断崖式下跌。我一般把 Tomcat 线程池大小调成 HikariCP 连接池大小的两到三倍避免死等。基础设施这块我最大的心得体会是Agent 项目的技术选型不是越新越好、不是越复杂越好。我见过不少团队把项目搭得非常宏大又是多模型网关又是分布式链路追踪结果连一个最简单的问答链路都跑不通。问数 Agent 的地基只需要把模型调用、工具执行、记忆读写、日志追踪这几件事做得扎实后面业务逻辑往上堆才能又快又稳。先把最小闭环跑通再逐步加复杂度这套方法在问数项目里已经被验证过两次了值得你照抄。