智能水族Agent

2026年06月21日 by zx

我做了一个智能水族 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,鱼浮头怎么办?

系统大致会这样处理:

  1. Router 抽取到水质参数:氨氮 0.5、pH 7.8
  2. 识别到“浮头”这类疾病或应激症状
  3. 同时分发到 WaterAgentDiseaseAgent
  4. WaterAgent 查询水质标准,判断氨氮超标并给出停食、增氧、换水等建议
  5. DiseaseAgent 检索缺氧应激、氨氮中毒等可能问题
  6. Router 合并多 Agent 结果
  7. 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 工程落到具体领域里的尝试。水族问题很适合做这件事:它既有自然语言的不确定性,也有水质参数、物种兼容性和设备配置这些明确的结构化知识。

当大模型不再单独回答所有问题,而是学会调用领域工具、尊重事实数据、记住上下文,它才开始真正像一个能帮上忙的智能助手。


评论 (0)

暂无评论,快来抢沙发吧!

发表评论



```