ToolBox

AI 应用开发实战教程

第 7 章 · 综合实战:智粮系统智能助手

8/9
教程/AI 应用开发实战教程/第 7 章 · 综合实战:智粮系统智能助手
8 节 / 共 9 AI 应用开发实战教程

第 7 章 · 综合实战:智粮系统智能助手

第 7 章 · 综合实战:智粮系统智能助手

本章目标:把前面所有技术(Prompt + 结构化输出 + RAG + Agent)串成一个完整可运行的应用。做完它,你就完成了"从零到一"的闭环——这也是你转型的"代表作"雏形。


7.1 我们要做什么

一个「智粮系统智能助手」,用户用自然语言就能干三件事:

  1. 查数据:问"玉米库存多少",它去查数据库。
  2. 问知识:问"玉米含水率超标怎么折算",它从企业知识库(RAG)找答案。
  3. 算结果:问"5000 公斤玉米含水率 14% 折成 12% 是多少",它调用折算工具算给你。

核心设计:不写死 if-else 分流,而是把这三件事都注册成"工具",交给 Agent 自己决定"该调哪个、怎么调"。这就是前面几章技术的真正用武之地。


7.2 完整代码

把下面整段保存为 assistant.py

import os
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"   # 国内下模型走镜像

import json
import sqlite3
from openai import OpenAI
from sentence_transformers import SentenceTransformer
import chromadb

# ============ 1. 初始化 ============
llm = OpenAI(api_key=os.environ.get("DEEPSEEK_API_KEY", "sk-你的key"), base_url="https://api.deepseek.com")
embedder = SentenceTransformer("BAAI/bge-small-zh-v1.5")

# 业务数据:库存表(真实项目换成你的 MySQL/PG)
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE stock (name TEXT, qty INTEGER)")
conn.executemany("INSERT INTO stock VALUES (?,?)",
                 [("玉米", 5000), ("小麦", 3000), ("水稻", 2000)])
conn.commit()

# 企业知识库:几段文档(真实项目换成你的文档,可上万段)
kb_docs = [
    "收粮入库流程:车辆过磅 → 抽样化验含水率 → 按标准折算 → 入库登记。玉米标准含水率 12%,小麦 13%。",
    "含水率折算公式:折合重量 = 实测重量 × (1-实测含水率)/(1-标准含水率)。",
    "称重系统并发写入可能出现数据不一致,需为地磅串口读取增加重试机制,并加分布式锁。",
]
kb = chromadb.Client().get_or_create_collection("kb")
kb.add(
    ids=[f"d{i}" for i in range(len(kb_docs))],
    documents=kb_docs,
    embeddings=[embedder.encode(d).tolist() for d in kb_docs],
)

# ============ 2. 工具实现(真正的业务逻辑在这里) ============
def query_stock(grain_name: str) -> str:
    """查库存"""
    row = conn.execute("SELECT qty FROM stock WHERE name=?", (grain_name,)).fetchone()
    return f"{grain_name} 当前库存 {row[0]} 公斤" if row else f"未找到 {grain_name} 的库存记录"

def search_kb(question: str) -> str:
    """从企业知识库检索最相关的资料(RAG)"""
    res = kb.query(
        query_embeddings=[embedder.encode(question).tolist()],
        n_results=2,
    )
    chunks = res["documents"][0]
    return "\n".join(f"- {c}" for c in chunks)

def calc_fold(actual_weight: float, actual_moisture: float, std_moisture: float) -> str:
    """含水率折算"""
    folded = actual_weight * (1 - actual_moisture) / (1 - std_moisture)
    return f"折合重量为 {folded:.1f} 公斤"

TOOL_MAP = {"query_stock": query_stock, "search_kb": search_kb, "calc_fold": calc_fold}

# ============ 3. 工具的 JSON 描述(告诉模型"有什么工具、怎么用") ============
tools = [
    {
        "type": "function",
        "function": {
            "name": "query_stock",
            "description": "查询某粮食品种的库存数量(公斤)",
            "parameters": {
                "type": "object",
                "properties": {"grain_name": {"type": "string", "description": "粮食品种,如 玉米、小麦、水稻"}},
                "required": ["grain_name"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_kb",
            "description": "从企业知识库检索收粮相关的规章制度、流程、公式",
            "parameters": {
                "type": "object",
                "properties": {"question": {"type": "string", "description": "要查询的问题"}},
                "required": ["question"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calc_fold",
            "description": "按含水率折算粮食重量。折合重量=实测重量×(1-实测含水率)/(1-标准含水率)",
            "parameters": {
                "type": "object",
                "properties": {
                    "actual_weight": {"type": "number", "description": "实测重量,公斤"},
                    "actual_moisture": {"type": "number", "description": "实测含水率,如 0.14 表示 14%"},
                    "std_moisture": {"type": "number", "description": "标准含水率,如 0.12 表示 12%"},
                },
                "required": ["actual_weight", "actual_moisture", "std_moisture"],
            },
        },
    },
]

# ============ 4. Agent 循环(决策 + 执行 + 兜底) ============
SYSTEM_PROMPT = (
    "你是智粮系统的智能助手。你可以调用工具查询库存、检索知识库、做含水率折算。\n"
    "规则:\n"
    "1. 优先用工具获取真实数据,不要凭空编造数字;\n"
    "2. 回答要简洁,用中文,直接给结论;\n"
    "3. 如果工具没查到,如实说明,不要编造。"
)

def chat(question: str, max_steps=5) -> str:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": question},
    ]

    for _ in range(max_steps):
        resp = llm.chat.completions.create(
            model="deepseek-v4-flash",
            messages=messages,
            tools=tools,
        )
        msg = resp.choices[0].message

        if msg.tool_calls:                       # 想调工具 → 执行,继续循环
            messages.append(msg)
            for call in msg.tool_calls:
                name = call.function.name
                args = json.loads(call.function.arguments)
                print(f"  [调用工具] {name}{args}")
                result = TOOL_MAP[name](**args)
                messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
            continue
        else:                                    # 不调工具了 → 返回最终答案
            return msg.content

    return "抱歉,任务步骤过多,未能完成。"

# ============ 5. 命令行入口 ============
if __name__ == "__main__":
    for q in [
        "玉米现在库存多少?",
        "玉米含水率超标了怎么折算?",
        "5000 公斤玉米,实测含水率 14%,按标准 12% 折算后是多少?",
    ]:
        print("\n用户:", q)
        print("助手:", chat(q))

7.3 跑起来看效果

pip install openai sentence-transformers chromadb
python assistant.py

你会看到类似这样的输出——注意模型自己决定了每个问题该调哪个工具:

用户: 玉米现在库存多少?
  [调用工具] query_stock{'grain_name': '玉米'}
助手: 玉米当前库存 5000 公斤。

用户: 玉米含水率超标了怎么折算?
  [调用工具] search_kb{'question': '玉米含水率超标怎么折算'}
助手: 根据知识库,含水率折算公式为:折合重量 = 实测重量 × (1-实测含水率)/(1-标准含水率)。
        玉米标准含水率 12%,小麦 13%。

用户: 5000 公斤玉米,实测含水率 14%,按标准 12% 折算后是多少?
  [调用工具] calc_fold{'actual_weight': 5000, 'actual_moisture': 0.14, 'std_moisture': 0.12}
助手: 折合重量为 4886.4 公斤。

这个例子浓缩了全部技术

  • 第一个问题 → 走了"查数据"(第 3 章的 SQL 能力,这里简化成查库存表)
  • 第二个问题 → 走了"RAG 知识库检索"(第 4 章)
  • 第三个问题 → 走了"工具调用 + 参数解析"(第 5 章的 Function Calling)

而"该走哪条路",是模型自己判断的——这就是 Agent。


7.4 加一个界面:用 Streamlit 10 分钟出成品

命令行不够"像产品"。用 Streamlit 加一个网页界面,几行代码的事:

pip install streamlit

新建 app.py

import streamlit as st
from assistant import chat          # 复用上面的 chat 函数

st.set_page_config(page_title="智粮系统智能助手")
st.title("🌾 智粮系统智能助手")

if "history" not in st.session_state:
    st.session_state.history = []

question = st.chat_input("用大白话问我:查库存、问制度、算折算……")
if question:
    st.session_state.history.append({"role": "user", "content": question})
    answer = chat(question)
    st.session_state.history.append({"role": "assistant", "content": answer})

for m in st.session_state.history:
    with st.chat_message(m["role"]):
        st.write(m["content"])

运行:

streamlit run app.py

浏览器自动打开,一个带聊天界面的智能助手就出来了。

注意:from assistant import chat 会执行 assistant.py 顶层的初始化代码(加载模型、建库),这是故意的——那些只需要在启动时执行一次。真实项目里你可以把初始化挪进一个 init() 函数,或加 if __name__ == "__main__" 保护。这里为了简单直接 import。


7.5 怎么把这个项目"变大"

这个 6.3 的雏形,可以按下面几条路继续长成真正的产品,每一条都对应你已学的技术:

  1. 知识库换真实文档:把 kb_docs 换成你公司真正的制度/规范/README,上千份也能塞(第 4 章)。
  2. 库存表换真数据库:把 sqlite 换成 MySQL/PG,query_stock 里写真实 SQL(第 3 章)。
  3. 加权限:工具执行前校验当前用户有没有权限查这个数据(第 8 章安全)。
  4. 加记忆:把用户历史存进向量库,实现"它记得你上次问过什么"(第 6 章)。
  5. 加评测:建 50 条标准问答,每次改 prompt 都回归测试(第 6 章)。

把上面任意两条做完并开源到 GitHub,你这个"转型代表作"就立住了。


本章小结

  • 综合项目 = Agent 循环 + 多个业务工具(查库 / RAG / 计算)。
  • "该调哪个工具"交给模型决策,业务逻辑和权限控制在你的代码里。
  • Streamlit 能 10 分钟给项目套上网页界面。
  • 这个雏形就是你所有学习成果的"集大成",也是面试/转岗时的最好证明。