我做了一个智能水族 Agent:让大模型真正接住养鱼问题
很多人第一次把大模型接进自己的项目时,最自然的做法是:用户提问,直接把问题丢给模型,然后把回答展示出来。这个方式跑 demo 很快,但放到水族场景里很容易出问题。
养鱼不是纯聊天。用户问“60cm 草缸预算 300,过滤和灯怎么配”,背后包含缸长、缸型、预算、设备类型等结构化信息;用户问“氨氮 0.5,pH 7.8 怎么办”,系统需要优先判断风险,而不是写一段漂亮但不落地的泛泛建议;用户问“七彩神仙和宝莲灯能不能一起养”,它又变成了物种参数和混养兼容性问题。
所以我做 SmartAquarium Full 时,没有把它设计成一个单纯的 ChatBot,而是把它拆成了一个面向水族领域的多 Agent 系统:大模型负责理解意图和组织表达,事实判断尽量交给工具、数据库和规则。
项目目标
SmartAquarium Full 是一个面向观赏鱼、水族缸、水质、鱼病、开缸和设备推荐的智能水族 Agent 系统。它目前支持:
- 鱼种百科查询
- 水质参数诊断
- 鱼病症状初判
- 混养兼容性判断
- 开缸流程指导
- 过滤、照明、加热、CO2、增氧等设备推荐
- 多轮会话记忆
- DeepSeek 意图识别与答案润色
- MCP 工具服务与 A2A 风格 Agent 服务调用
- SQLite / MySQL 双数据库支持
本地知识库目前包含 191 条鱼种记录、14962 条混养兼容性记录、26 条疾病记录、385 条水质标准、34 条开缸步骤和 36 条设备推荐规则。
整体架构
系统的核心链路可以概括为:用户问题先进入 Router,Router 识别意图并抽取槽位,然后分发给对应的专业 Agent。Agent 不直接臆测答案,而是调用工具层查询事实数据,最后再让大模型把事实结果润色成自然、专业的中文回答。
flowchart TD
U["用户 CLI"] --> R["Router 意图识别与分发"]
R --> M["会话记忆"]
R --> F["FishAgent 鱼种百科"]
R --> W["WaterAgent 水质诊断"]
R --> D["DiseaseAgent 鱼病初判"]
R --> T["TankAgent 混养 / 开缸 / 设备"]
F --> FM["fish_mcp"]
W --> WM["water_mcp"]
D --> DM["disease_mcp"]
T --> TM["tank_mcp"]
FM --> DB["业务数据库"]
WM --> DB
DM --> DB
TM --> DB
F --> L["DeepSeek 答案润色"]
W --> L
D --> L
T --> L
L --> U
这里最重要的取舍是:大模型不直接充当数据库。它可以理解自然语言、补全表达、压缩上下文,但鱼种参数、水质范围、疾病处理建议和设备推荐都来自结构化数据。
Router:把一句自然语言拆成可执行任务
水族问题经常是混合意图。例如:
我有一个 60cm 草缸,预算 300,想养黑壳虾,过滤和灯怎么配?水质 pH 7.8 可以吗?
这不是一个单一问题。里面至少包含:
- 鱼缸条件:60cm、草缸、预算 300
- 生物目标:黑壳虾
- 设备需求:过滤、照明
- 水质判断:pH 7.8
Router 会先使用 DeepSeek 做意图识别和槽位抽取;如果没有配置模型,或者模型调用失败,就回退到规则识别。这个兜底设计很关键,因为领域系统不能因为 LLM 不可用就完全瘫痪。
目前 Router 支持的意图包括:
fish_info:鱼种百科water_quality:水质诊断disease:疾病初判compatibility:混养兼容tank_setup:开缸指导equipment:设备推荐chit_chat:简单问候out_of_scope:非水族问题拦截
我还加了一个 process_with_trace 接口,用来返回路由轨迹。这样调试时可以看到问题被识别成了什么意图、分发给了哪个 Agent、背后调用了哪个工具服务。
多 Agent:让每个模块只做自己擅长的事
系统里有四类核心 Agent:
FishAgent:负责鱼种百科和新手选鱼WaterAgent:负责氨氮、亚硝酸盐、硝酸盐、pH、温度等水质诊断DiseaseAgent:负责根据白点、烂鳍、白便、蒙眼、浮头等症状做初步判断TankAgent:负责混养、开缸流程和设备推荐
每个 Agent 的工作方式都很克制:先调用工具拿事实,再生成草稿,最后可选地调用 DeepSeek 润色。比如 FishAgent 如果识别到鱼名,就查具体鱼种;如果没有鱼名,就按难度、科属、性情、缸长筛选候选鱼。
这个设计让系统更容易维护。将来如果我要增强鱼病模块,不需要改 Router 之外的大量逻辑,只需要扩展疾病库和 DiseaseAgent。如果我要换模型,也不会影响工具层和数据层。
MCP 工具服务:把领域能力变成可调用工具
项目暴露了四组 MCP 工具服务:
fish_mcp:鱼种查询和筛选water_mcp:水质诊断disease_mcp:症状检索tank_mcp:混养、开缸和设备推荐
多 Agent 模式下,它们分别运行在 6101 到 6104 端口;Agent 服务运行在 5101 到 5104 端口;Router 服务运行在 6600 端口。
我专门写了一个 MCP 客户端封装,用线程维护异步 MCP session,并做连接复用。调用工具时,上层 Agent 只需要像调用普通 Python 方法一样调用:
rows = self.tools.get_fish(fish_name)
这层抽象的好处是,Agent 不需要关心底层是本地函数、HTTP 服务还是 MCP 协议。后续如果要把工具服务拆到别的机器上,也不会破坏 Agent 的业务代码。
数据层:用事实库降低幻觉
水族建议最怕“听起来很专业,但参数是错的”。所以我把知识拆成了几张结构化表:
fish_info:鱼种百科、体型、温度、pH、性情、缸长等compatibility:物种混养兼容性disease_lib:疾病症状、紧急程度、处理建议和预防方式water_standards:通用和物种级水质标准tank_guide:不同缸型的开缸流程equipment:按缸长、预算和设备类型推荐配置
数据生成也做成了独立管线。鱼种基础清单来自本地 CSV,并通过 GBIF Species Match API 做物种匹配;水质标准、混养判断、疾病库、开缸流程和设备推荐则由本地规则生成,再导入数据库。
这里我没有追求一开始就“全自动爬完所有知识”。更实际的做法是:公开数据补物种分类,本地规则保证饲养建议可控,后面再逐步人工校正高频鱼种和高风险疾病。
会话记忆:让“那我的过滤呢?”能被理解
水族咨询天然是多轮的。用户经常先说:
我有一个 60cm 草缸,预算 300,想养黑壳虾。
下一轮又问:
那过滤怎么配?
如果系统没有记忆,第二句里的“过滤”就缺少上下文。SmartAquarium 的记忆分为三层:
- 结构化事实:鱼种、缸型、缸长、预算、水质参数、症状、设备关注点
- 最近对话:保留最近 12 轮问答
- 摘要记忆:超过窗口后,用 DeepSeek 把旧对话压缩成不超过 300 字的长期摘要
记忆不是只存在内存里,而是持久化到数据库。命令行里可以通过不同 session_id 区分不同鱼缸,例如客厅缸和虾缸可以分别维护上下文。
这让系统更接近真实用户的使用方式:不是一次性问答,而是围绕同一个鱼缸持续咨询。
LLM 的位置:理解和表达,而不是替代事实
我在系统里把 DeepSeek 放在三个位置:
第一,Router 层做意图识别和槽位抽取。自然语言的变化太多,完全靠规则会很僵硬,大模型可以提高识别弹性。
第二,Agent 层做答案润色。工具返回的是事实结果,但直接展示数据库字段不够自然。大模型可以把结构化结果整理成更像水族顾问的回答。
第三,Memory 层做历史摘要。当对话超过最近窗口时,大模型负责压缩旧对话,保留鱼缸条件、鱼种、水质、预算、设备偏好和疾病症状等长期信息。
但系统同时保留规则兜底:没有 API Key、模型超时或调用失败时,仍然可以完成基本识别和模板回答。这也是我认为 Agent 工程里很重要的一点:模型能力是增强项,不应该是唯一支点。
一个典型问题的执行过程
假设用户问:
氨氮 0.5,pH 7.8,鱼浮头怎么办?
系统大致会这样处理:
- Router 抽取到水质参数:氨氮 0.5、pH 7.8
- 识别到“浮头”这类疾病或应激症状
- 同时分发到
WaterAgent和DiseaseAgent WaterAgent查询水质标准,判断氨氮超标并给出停食、增氧、换水等建议DiseaseAgent检索缺氧应激、氨氮中毒等可能问题- Router 合并多 Agent 结果
- DeepSeek 基于事实结果润色最终回答
这样的回答会比单纯问模型更稳,因为关键判断来自工具和数据库,而不是模型临场发挥。
工程上的几个取舍
第一个取舍是本地单进程和多服务模式同时保留。开发调试时可以直接运行 python main.py,部署或演示多 Agent 架构时可以运行 python scripts/start_multiagent.py。这样既方便快速测试,也保留了服务化扩展空间。
第二个取舍是 SQLite 和 MySQL 双支持。小规模开发时 SQLite 足够轻;需要持久化多用户会话和更正式的数据管理时,可以切到 MySQL。
第三个取舍是规则和模型并行。水族领域里有很多明确阈值和风险判断,例如氨氮、亚硝酸盐、pH 波动、温差应激等,这些不应该完全交给模型。模型适合做“语言理解”和“答案表达”,规则适合做“边界判断”和“安全兜底”。
第四个取舍是先做可解释的粗粒度系统,而不是一开始追求复杂 RAG。当前数据量还不算大,结构化查询比向量检索更直接,也更容易解释为什么给出某个建议。
下一步计划
后面我想继续做几件事:
- 增加 Web UI,让用户可以用表单和对话两种方式输入水质、鱼种和缸体信息
- 给每次回答展示 trace,让用户知道建议来自哪个 Agent 和哪些数据
- 扩展疾病库,加入更多检疫、隔离、药物禁忌和物种耐受性说明
- 引入图片识别,辅助判断白点、烂鳍、蒙眼、腹水等可视症状
- 增加定时提醒,例如换水、测水、清洗前置棉、检疫结束提醒
- 对高风险建议增加更严格的安全策略,避免误导用户乱下药
SmartAquarium Full 对我来说不是一个“AI 养鱼助手”的简单 demo,而是一次把 Agent 工程落到具体领域里的尝试。水族问题很适合做这件事:它既有自然语言的不确定性,也有水质参数、物种兼容性和设备配置这些明确的结构化知识。
当大模型不再单独回答所有问题,而是学会调用领域工具、尊重事实数据、记住上下文,它才开始真正像一个能帮上忙的智能助手。