由 AI 驱动
  • 主页
  • 手册
    • SQL 手册
    • R 手册
    • Python 手册
    • 机器学习手册
    • TensorFlow 手册
    • AI 手册
  • 博客
  • CV / 简历

On this page

  • 项目概览
    • 核心架构
      • 多 Agent AI 系统
      • 技术栈
    • 数据库架构
      • 数据模式与来源
      • 数据采集流程
    • AI/ML 实现
      • 向量嵌入技术
      • RAG 系统架构
      • RAG 工作流
      • 相似度搜索实现
    • 专门 Agent 的实现
      • DrunkTony (中文 Agent)
      • WhiskyFunny (英文 Agent)
    • 推荐引擎
      • 双 LLM 推荐系统
    • 用户界面设计
      • Streamlit 实现
    • 频率限制与成本管理
      • API 使用控制
    • 性能优化
      • 向量数据库性能
    • 部署与配置
      • 环境搭建
      • Shiny for Python 备选方案
    • 技术成就
      • 核心创新
      • 性能指标
    • 未来增强方向
    • 结论

基于 RAG 的威士忌品鉴笔记

  • Show All Code
  • Hide All Code

  • View Source
AI
API
tutorial
Author

Tony D

Published

November 5, 2025

项目概览

威士忌品鉴应用是一款先进的 AI 驱动 Web 应用程序,可生成详细、专业的威士忌品鉴笔记和建议。该系统的特别之处在于其采用了多 Agent 架构,并配备了专门的 AI 人格,每个人格都经过了不同威士忌评论源和语言风格的训练。

在线演示 (Modelscope): https://modelscope.cn/studios/ttflying/whisky_AI_tasting

在线演示 (Shinyapp): https://jcflyingco.shinyapps.io/ai-whisky-tasting/

Github: https://github.com/JCwinning/whisky_tasting

  • AI 品鉴词
  • AI 推荐

核心架构

多 Agent AI 系统

该应用包含三个专门的 AI Agent,每个 Agent 都有独特的特征和数据源:

  1. DrunkTony (dt) - 中文 Agent,专注于中文威士忌评论。
  2. WhiskyFunny (wf) - 英文 Agent,数据源自 whiskyfun.com。
  3. WhiskyNotebook (wn) - 英文 Agent,数据源自 whiskynotes.be。

技术栈

  • 主要语言: Python 3.13+
  • Web 框架: Streamlit (主选) + Shiny for Python (备选)
  • 数据库: DuckDB (376MB,针对向量操作进行了优化)
  • AI/ML: OpenAI API, 向量嵌入 (Vector embeddings), RAG 系统
  • 数据源: 使用 BeautifulSoup4 进行网页抓取

数据库架构

数据模式与来源

系统使用 DuckDB 作为主数据库,因其在向量操作方面表现卓越:

-- 数据库结构
CREATE TABLE drinktony_embed (
    full TEXT,
    bottle_embedding FLOAT[1024]  -- 1024 维向量
);

CREATE TABLE whiskyfun_embed (
    full TEXT,
    bottle_embedding FLOAT[1024]
);

CREATE TABLE whiskynote_embed (
    full TEXT,
    bottle_embedding FLOAT[1024]
);

数据采集流程

Code
# get_data_dt.py 中的网页抓取示例
def scrape_drinktony_reviews():
    """从 drinktony.netlify.app 抓取中文威士忌评论"""
    url = "https://drinktony.netlify.app/"
    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")

    # 查找所有评论链接
    post_links = [urljoin(url, a.get("href"))
                 for a in soup.select("a.quarto-grid-link")]

    all_reviews = []
    for post_url in post_links:
        response = requests.get(post_url)
        soup = BeautifulSoup(response.content, "html.parser")

        # 提取评论章节
        review_sections = soup.select("section.level1")
        for section in review_sections:
            # 解析威士忌名称和评论内容
            whisky_name = extract_whisky_name(section)
            review_text = extract_review_text(section)
            all_reviews.append({
                'whisky': whisky_name,
                'review': review_text
            })

    return all_reviews

AI/ML 实现

向量嵌入技术

应用使用尖端的嵌入技术将文本转换为高维向量,以便进行语义相似度搜索。

嵌入模型详情

模型: BAAI/bge-large-zh-v1.5 - 维度: 1024 - 服务商: SiliconFlow API - 用途: 跨语言文本理解(在中英文方面表现均很出色)

为什么选择 BGE-Large-ZH?

  1. 跨语言能力: 在理解中英文威士忌专业术语方面表现优异。
  2. 高性能: 与通用嵌入模型相比,具备更出色的语义理解能力。
  3. 高效尺寸: 1024 维度在性能和存储效率之间达到了平衡。
  4. API 访问: 通过 SiliconFlow 提供稳定的服务。

嵌入流程

该应用使用 BGE-Large-ZH-v1.5 模型生成嵌入向量:

Code
def get_embedding(text: str, api_key: str):
    """使用 SiliconFlow API 生成文本嵌入"""
    client = OpenAI(
        api_key=api_key,
        base_url="https://api.siliconflow.cn/v1"
    )
    response = client.embeddings.create(
        model="BAAI/bge-large-zh-v1.5",
        input=[text]
    )
    return np.array(response.data[0].embedding)

def cosine_similarity(v1, v2):
    """计算两个向量之间的余弦相似度"""
    if v1 is None or v2 is None:
        return 0
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))

RAG 系统架构

检索增强生成 (RAG) 系统是此应用的核心创新。让我们详细探讨其组件。

RAG 工作流

检索增强生成过程遵循以下步骤:

%%| fig-cap: "威士忌品鉴的 RAG 工作流"
flowchart TD
    A[用户输入<br/>威士忌名称] --> B[生成嵌入向量]
    B --> C[DuckDB 中的<br/>向量相似度搜索]
    C --> D[检索前 10 条<br/>相似评论]
    D --> E[为 LLM 格式化上下文]
    E --> F[主 LLM<br/>生成品鉴笔记]
    F --> G[副 LLM<br/>生成推荐建议]
    G --> H[格式化并<br/>显示结果]

    C --> I[数据库]
    I --> J[drinktony_embed<br/>1200 条中文评论]
    I --> K[whiskyfun_embed<br/>2 万条英文评论]
    I --> L[whiskynote_embed<br/>5000 条英文评论]

相似度搜索实现

Code
def find_similar_chunks(
    query_embedding,
    db_path,
    table_name,
    text_col,
    embedding_col,
    top_n=10,
):
    """使用余弦相似度在数据库中查找最相似的文本块"""
    try:
        with duckdb.connect(database=db_path, read_only=True) as con:
            df = con.execute(
                f'SELECT "{text_col}", "{embedding_col}" FROM "{table_name}"'
            ).fetchdf()

            # 计算相似度
            similarities = []
            for _, row in df.iterrows():
                text = row[text_col]
                embedding = np.array(row[embedding_col])
                similarity = cosine_similarity(query_embedding, embedding)
                similarities.append((text, similarity))

            # 按相似度排序并返回前 N 条
            similarities.sort(key=lambda x: x[1], reverse=True)
            return similarities[:top_n]

    except Exception as e:
        print(f"搜索相似块时出错: {e}")
        return []

专门 Agent 的实现

DrunkTony (中文 Agent)

Code
def run_conversation(query, api_key, model):
    """生成中文格式的威士忌品鉴笔记"""

    # 第 1 步:生成嵌入并查找相似评论
    query_embedding = get_embedding(query, api_key)
    similar_reviews = find_similar_chunks(
        query_embedding=query_embedding,
        db_path="data/whisky_database.duckdb",
        table_name="drinktony_embed",
        text_col="full",
        embedding_col="bottle_embedding"
    )

    # 第 2 步:为 LLM 准备上下文
    context = "\n---\n".join([review[0] for review in similar_reviews])

    # 第 3 步:生成品鉴笔记
    prompt = f"""作为威士忌专家,基于以下威士忌品鉴笔记,为"{query}"生成专业的品鉴报告:

参考品鉴笔记:
{context}

请按以下格式输出:
{query}
闻香: [详细描述]
品味: [详细描述]
打分: [90-100分]
"""

    response = llm_client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": "你是一位专业的威士忌品鉴师,擅长生成详细准确的品鉴笔记。"},
            {"role": "user", "content": prompt}
        ],
        temperature=0.7,
        max_tokens=1000
    )

    return response.choices[0].message.content

WhiskyFunny (英文 Agent)

Code
def run_conversation(query, api_key, model):
    """生成英文格式的威士忌品鉴笔记"""

    query_embedding = get_embedding(query, api_key)
    similar_reviews = find_similar_chunks(
        query_embedding=query_embedding,
        db_path="data/whisky_database.duckdb",
        table_name="whiskyfun_embed",
        text_col="full",
        embedding_col="bottle_embedding"
    )

    context = "\n---\n".join([review[0] for review in similar_reviews])

    prompt = f"""As a whisky expert, generate professional tasting notes for "{query}" based on these reference reviews:

Reference reviews:
{context}

Please output in this format:
Colour: [detailed description]
Nose: [detailed aroma description]
Mouth: [detailed taste description]
Finish: [detailed finish description]
Comments: [overall impression]
SGP: xxx - xx points
"""

    response = llm_client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": "You are a professional whisky taster specializing in detailed sensory analysis."},
            {"role": "user", "content": prompt}
        ],
        temperature=0.7,
        max_tokens=1200
    )

    return response.choices[0].message.content

推荐引擎

双 LLM 推荐系统

应用使用一个独立的 LLM 来生成威士忌推荐建议:

Code
def recommend_whiskies_by_profile(tasting_notes, api_key, model):
    """基于品鉴特征生成威士忌推荐"""

    prompt = f"""Based on these tasting notes, recommend 2 similar whiskies that the user might enjoy:

{Tasting Notes:}
{tasting_notes}

Please provide:
1. Whisky name with brief description
2. Why it matches the user's preference
3. Price range and availability

Format each recommendation as:
**Recommendation [1/2]:** [Whisky Name]
**Why it matches:** [detailed reasoning]
**Details:** [price, availability, tasting profile]
"""

    response = recommendation_client.chat.completions.create(
        model=model,  # 推荐使用不同的模型
        messages=[
            {"role": "system", "content": "You are a whisky recommendation expert with deep knowledge of global whisky brands and profiles."},
            {"role": "user", "content": prompt}
        ],
        temperature=0.8,
        max_tokens=800
    )

    return response.choices[0].message.content

用户界面设计

Streamlit 实现

Code
# 主应用程序界面
def main():
    st.set_page_config(
        page_title="AI 威士忌品鉴系统",
        page_icon="🥃",
        layout="wide"
    )

    # 侧边栏 Agent 选择
    with st.sidebar:
        st.header("🥃 威士忌 AI 品鉴")

        # Agent 选择
        agent_type = st.selectbox(
            "选择品鉴 Agent:",
            ["DrunkTony (中文)", "WhiskyFunny (English)", "WhiskyNotebook (English)"],
            help="每个 Agent 都有不同的性格和数据源"
        )

        # 模型选择
        model_options = get_model_options(agent_type)
        selected_model = st.selectbox("模型:", model_options)

    # 主内容区域
    st.header("专业的威士忌品鉴笔记生成器")

    # 输入部分
    col1, col2 = st.columns([2, 1])
    with col1:
        whisky_name = st.text_input(
            "输入威士忌名称:",
            placeholder="例如: Macallan 18 Year Old",
            help="输入完整的威士忌名称,包括年份和桶型"
        )

    with col2:
        st.write("")  # 间距
        generate_btn = st.button("🍷 生成品鉴笔记", type="primary")
        recommend_btn = st.button("🎯 获取推荐")

    # 输出部分
    if generate_btn:
        if not whisky_name:
            st.error("请输入威士忌名称")
        else:
            with st.spinner("正在分析威士忌..."):
                tasting_notes = generate_tasting_notes(whisky_name, agent_type, selected_model)
                st.markdown("### 🥃 品鉴笔记")
                st.markdown(tasting_notes)

    if recommend_btn:
        with st.spinner("正在查找推荐..."):
            recommendations = get_recommendations(tasting_notes, agent_type)
            st.markdown("### 🎯 个性化推荐")
            st.markdown(recommendations)

频率限制与成本管理

API 使用控制

Code
# 频率限制实现
RATE_LIMIT_FILE = "rate_limit.json"
MAX_RUNS_PER_DAY = 80

def get_rate_limit_data():
    """获取当前使用数据"""
    try:
        with open(RATE_LIMIT_FILE, "r") as f:
            data = json.load(f)
            # 确保键存在
            if "date" not in data or "count" not in data:
                return {"date": str(datetime.date.today()), "count": 0}
            return data
    except (FileNotFoundError, json.JSONDecodeError):
        return {"date": str(datetime.date.today()), "count": 0}

def check_rate_limit():
    """检查用户是否超出了每日限制"""
    data = get_rate_limit_data()
    today = str(datetime.date.today())

    if data["date"] != today:
        # 为新的一天重置计数器
        return {"date": today, "count": 0, "can_run": True}

    if data["count"] >= MAX_RUNS_PER_DAY:
        return {"date": today, "count": data["count"], "can_run": False}

    return {"date": today, "count": data["count"], "can_run": True}

性能优化

向量数据库性能

Code
# 针对大数据的分批优化相似度搜索
def batch_similarity_search(query_embedding, db_path, table_name, batch_size=1000):
    """针对大型数据集的分批相似度搜索优化"""

    with duckdb.connect(database=db_path, read_only=True) as con:
        # 获取总行数
        total_rows = con.execute(f"SELECT COUNT(*) FROM {table_name}").fetchone()[0]

        all_similarities = []

        # 分批处理以管理内存
        for offset in range(0, total_rows, batch_size):
            batch_df = con.execute(
                f'SELECT full, bottle_embedding FROM {table_name} LIMIT {batch_size} OFFSET {offset}'
            ).fetchdf()

            # 计算该批次的相似度
            for _, row in batch_df.iterrows():
                embedding = np.array(row['bottle_embedding'])
                similarity = cosine_similarity(query_embedding, embedding)
                all_similarities.append((row['full'], similarity))

        # 排序并返回前几条结果
        all_similarities.sort(key=lambda x: x[1], reverse=True)
        return all_similarities[:10]

部署与配置

环境搭建

# .env 文件中的环境变量
SILICONFLOW_API_KEY=您的 SiliconFlow 密钥
MODELSCOPE_API_KEY=您的 ModelScope 密钥
GEMINI_API_KEY=您的 Gemini 密钥  # 可选

# 数据库初始化
python -c "
import duckdb
conn = duckdb.connect('data/whisky_database.duckdb')
# 创建支持向量的表格
"

Shiny for Python 备选方案

Code
# 使用 Shiny 实现的备选 UI
from shiny import App, ui, render, reactive, req

app_ui = ui.page_fluid(
    ui.h2("🥃 AI 威士忌品鉴系统"),
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_select("agent", "选择 Agent:", [
                "DrunkTony (中文)", "WhiskyFunny (English)", "WhiskyNotebook (English)"
            ]),
            ui.input_text("whisky_name", "威士忌名称:", ""),
            ui.input_action_button("generate", "生成品鉴笔记")
        ),
        ui.output_text_verbatim("tasting_notes"),
        ui.output_text_verbatim("recommendations")
    )
)

def server(input, output, session):
    @output
    @render.text
    def tasting_notes():
        req(input.generate())
        # 生成品鉴笔记逻辑
        return generate_tasting_notes(input.whisky_name(), input.agent())

app = App(app_ui, server)

技术成就

核心创新

  1. 多 Agent 架构: 拥有不同的 AI 人格和数据源。
  2. RAG 实现: 使用真实威士忌评论进行检索增强生成。
  3. 向量数据库: 使用 376MB 数据库进行高效相似度搜索。
  4. 双语支持: 提供中文和英文 Agent。
  5. 成本管理: 内置频率限制和 API 优化。
  6. 专业品鉴格式: 符合行业标准的笔记结构。

性能指标

指标 数值
数据库大小 376MB (26,000+ 条评论)
嵌入维度 1024
搜索延迟 < 2 秒
每日请求限制 80 次请求
支持语言 2 (EN/ZH)
数据源 3 (drinktony, whiskyfun, whiskynotes)

未来增强方向

下一版本的潜在改进点:

  1. 高级相似度算法: 实现文本 + 元数据的混合搜索。
  2. 用户个性化: 随着时间推移学习用户的偏好。
  3. 移动端应用: 原生的 iOS/Android 应用程序。
  4. 实时数据更新: 自动化的网页抓取流水线。
  5. 口味特征分析: 根据用户偏好生成其口味特征图谱。
  6. 社交功能: 与社区分享品鉴笔记和推荐。

结论

这款威士忌品鉴应用展示了将 RAG 技术与专门的 AI Agent 相结合,从而构建复杂的领域特定系统的强大力量。本项目展示了:

  • 先进的 AI 架构: 具备不同人格的多 Agent 系统。
  • 数据工程: 大规模向量数据库管理。
  • 专业知识: 行业标准的品鉴笔记格式。
  • 用户体验: 跨平台的简洁、直观界面。
  • 成本效益: 智能的频率限制和优化。

无论您是威士忌爱好者、AI 开发人员还是数据科学家,本项目都为您提供了一个极佳的范例,展示了如何结合真实世界数据与先进机器学习技术来构建生产级 AI 应用。


技术栈: Python, Streamlit, DuckDB, OpenAI API, 向量嵌入 (Vector Embeddings)

数据库: 涵盖 3 个专门来源的 26,000+ 条真实威士忌评论。

Source Code
---
title: "基于 RAG 的威士忌品鉴笔记"
author: "Tony D"
date: "2025-11-05"
categories: [AI, API, tutorial]
image: "images/0.png"

format:
  html:
    code-fold: true
    code-tools: true
    code-copy: true

execute:
  eval: false
  warning: false

  
  
---


# 项目概览

威士忌品鉴应用是一款先进的 AI 驱动 Web 应用程序,可生成详细、专业的威士忌品鉴笔记和建议。该系统的特别之处在于其采用了多 Agent 架构,并配备了专门的 AI 人格,每个人格都经过了不同威士忌评论源和语言风格的训练。

在线演示 (Modelscope): [https://modelscope.cn/studios/ttflying/whisky_AI_tasting](https://modelscope.cn/studios/ttflying/whisky_AI_tasting)

在线演示 (Shinyapp): [https://jcflyingco.shinyapps.io/ai-whisky-tasting/](https://jcflyingco.shinyapps.io/ai-whisky-tasting)

Github: [https://github.com/JCwinning/whisky_tasting](https://github.com/JCwinning/whisky_tasting)



::: {.panel-tabset}




## AI 品鉴词
![](images/0.png){width="100%"}


## AI 推荐

![](images/1.png){width="100%"}


::: 


## 核心架构

### 多 Agent AI 系统

该应用包含三个专门的 AI Agent,每个 Agent 都有独特的特征和数据源:

1. **DrunkTony (dt)** - 中文 Agent,专注于中文威士忌评论。
2. **WhiskyFunny (wf)** - 英文 Agent,数据源自 whiskyfun.com。
3. **WhiskyNotebook (wn)** - 英文 Agent,数据源自 whiskynotes.be。

### 技术栈
- **主要语言**: Python 3.13+
- **Web 框架**: Streamlit (主选) + Shiny for Python (备选)
- **数据库**: DuckDB (376MB,针对向量操作进行了优化)
- **AI/ML**: OpenAI API, 向量嵌入 (Vector embeddings), RAG 系统
- **数据源**: 使用 BeautifulSoup4 进行网页抓取

## 数据库架构

### 数据模式与来源

系统使用 DuckDB 作为主数据库,因其在向量操作方面表现卓越:

```sql
-- 数据库结构
CREATE TABLE drinktony_embed (
    full TEXT,
    bottle_embedding FLOAT[1024]  -- 1024 维向量
);

CREATE TABLE whiskyfun_embed (
    full TEXT,
    bottle_embedding FLOAT[1024]
);

CREATE TABLE whiskynote_embed (
    full TEXT,
    bottle_embedding FLOAT[1024]
);
```

### 数据采集流程

```{python}
# get_data_dt.py 中的网页抓取示例
def scrape_drinktony_reviews():
    """从 drinktony.netlify.app 抓取中文威士忌评论"""
    url = "https://drinktony.netlify.app/"
    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")

    # 查找所有评论链接
    post_links = [urljoin(url, a.get("href"))
                 for a in soup.select("a.quarto-grid-link")]

    all_reviews = []
    for post_url in post_links:
        response = requests.get(post_url)
        soup = BeautifulSoup(response.content, "html.parser")

        # 提取评论章节
        review_sections = soup.select("section.level1")
        for section in review_sections:
            # 解析威士忌名称和评论内容
            whisky_name = extract_whisky_name(section)
            review_text = extract_review_text(section)
            all_reviews.append({
                'whisky': whisky_name,
                'review': review_text
            })

    return all_reviews
```

## AI/ML 实现

### 向量嵌入技术

应用使用尖端的嵌入技术将文本转换为高维向量,以便进行语义相似度搜索。

#### 嵌入模型详情

**模型**: BAAI/bge-large-zh-v1.5
- **维度**: 1024
- **服务商**: SiliconFlow API
- **用途**: 跨语言文本理解(在中英文方面表现均很出色)

#### 为什么选择 BGE-Large-ZH?

1. **跨语言能力**: 在理解中英文威士忌专业术语方面表现优异。
2. **高性能**: 与通用嵌入模型相比,具备更出色的语义理解能力。
3. **高效尺寸**: 1024 维度在性能和存储效率之间达到了平衡。
4. **API 访问**: 通过 SiliconFlow 提供稳定的服务。

#### 嵌入流程

该应用使用 BGE-Large-ZH-v1.5 模型生成嵌入向量:

```{python}
def get_embedding(text: str, api_key: str):
    """使用 SiliconFlow API 生成文本嵌入"""
    client = OpenAI(
        api_key=api_key,
        base_url="https://api.siliconflow.cn/v1"
    )
    response = client.embeddings.create(
        model="BAAI/bge-large-zh-v1.5",
        input=[text]
    )
    return np.array(response.data[0].embedding)

def cosine_similarity(v1, v2):
    """计算两个向量之间的余弦相似度"""
    if v1 is None or v2 is None:
        return 0
    return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
```

### RAG 系统架构

检索增强生成 (RAG) 系统是此应用的核心创新。让我们详细探讨其组件。

### RAG 工作流

检索增强生成过程遵循以下步骤:

```mermaid
%%| fig-cap: "威士忌品鉴的 RAG 工作流"
flowchart TD
    A[用户输入<br/>威士忌名称] --> B[生成嵌入向量]
    B --> C[DuckDB 中的<br/>向量相似度搜索]
    C --> D[检索前 10 条<br/>相似评论]
    D --> E[为 LLM 格式化上下文]
    E --> F[主 LLM<br/>生成品鉴笔记]
    F --> G[副 LLM<br/>生成推荐建议]
    G --> H[格式化并<br/>显示结果]

    C --> I[数据库]
    I --> J[drinktony_embed<br/>1200 条中文评论]
    I --> K[whiskyfun_embed<br/>2 万条英文评论]
    I --> L[whiskynote_embed<br/>5000 条英文评论]
```

### 相似度搜索实现

```{python}
def find_similar_chunks(
    query_embedding,
    db_path,
    table_name,
    text_col,
    embedding_col,
    top_n=10,
):
    """使用余弦相似度在数据库中查找最相似的文本块"""
    try:
        with duckdb.connect(database=db_path, read_only=True) as con:
            df = con.execute(
                f'SELECT "{text_col}", "{embedding_col}" FROM "{table_name}"'
            ).fetchdf()

            # 计算相似度
            similarities = []
            for _, row in df.iterrows():
                text = row[text_col]
                embedding = np.array(row[embedding_col])
                similarity = cosine_similarity(query_embedding, embedding)
                similarities.append((text, similarity))

            # 按相似度排序并返回前 N 条
            similarities.sort(key=lambda x: x[1], reverse=True)
            return similarities[:top_n]

    except Exception as e:
        print(f"搜索相似块时出错: {e}")
        return []
```

## 专门 Agent 的实现

### DrunkTony (中文 Agent)

```{python}
def run_conversation(query, api_key, model):
    """生成中文格式的威士忌品鉴笔记"""

    # 第 1 步:生成嵌入并查找相似评论
    query_embedding = get_embedding(query, api_key)
    similar_reviews = find_similar_chunks(
        query_embedding=query_embedding,
        db_path="data/whisky_database.duckdb",
        table_name="drinktony_embed",
        text_col="full",
        embedding_col="bottle_embedding"
    )

    # 第 2 步:为 LLM 准备上下文
    context = "\n---\n".join([review[0] for review in similar_reviews])

    # 第 3 步:生成品鉴笔记
    prompt = f"""作为威士忌专家,基于以下威士忌品鉴笔记,为"{query}"生成专业的品鉴报告:

参考品鉴笔记:
{context}

请按以下格式输出:
{query}
闻香: [详细描述]
品味: [详细描述]
打分: [90-100分]
"""

    response = llm_client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": "你是一位专业的威士忌品鉴师,擅长生成详细准确的品鉴笔记。"},
            {"role": "user", "content": prompt}
        ],
        temperature=0.7,
        max_tokens=1000
    )

    return response.choices[0].message.content
```

### WhiskyFunny (英文 Agent)

```{python}
def run_conversation(query, api_key, model):
    """生成英文格式的威士忌品鉴笔记"""

    query_embedding = get_embedding(query, api_key)
    similar_reviews = find_similar_chunks(
        query_embedding=query_embedding,
        db_path="data/whisky_database.duckdb",
        table_name="whiskyfun_embed",
        text_col="full",
        embedding_col="bottle_embedding"
    )

    context = "\n---\n".join([review[0] for review in similar_reviews])

    prompt = f"""As a whisky expert, generate professional tasting notes for "{query}" based on these reference reviews:

Reference reviews:
{context}

Please output in this format:
Colour: [detailed description]
Nose: [detailed aroma description]
Mouth: [detailed taste description]
Finish: [detailed finish description]
Comments: [overall impression]
SGP: xxx - xx points
"""

    response = llm_client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": "You are a professional whisky taster specializing in detailed sensory analysis."},
            {"role": "user", "content": prompt}
        ],
        temperature=0.7,
        max_tokens=1200
    )

    return response.choices[0].message.content
```

## 推荐引擎

### 双 LLM 推荐系统

应用使用一个独立的 LLM 来生成威士忌推荐建议:

```{python}
def recommend_whiskies_by_profile(tasting_notes, api_key, model):
    """基于品鉴特征生成威士忌推荐"""

    prompt = f"""Based on these tasting notes, recommend 2 similar whiskies that the user might enjoy:

{Tasting Notes:}
{tasting_notes}

Please provide:
1. Whisky name with brief description
2. Why it matches the user's preference
3. Price range and availability

Format each recommendation as:
**Recommendation [1/2]:** [Whisky Name]
**Why it matches:** [detailed reasoning]
**Details:** [price, availability, tasting profile]
"""

    response = recommendation_client.chat.completions.create(
        model=model,  # 推荐使用不同的模型
        messages=[
            {"role": "system", "content": "You are a whisky recommendation expert with deep knowledge of global whisky brands and profiles."},
            {"role": "user", "content": prompt}
        ],
        temperature=0.8,
        max_tokens=800
    )

    return response.choices[0].message.content
```

## 用户界面设计

### Streamlit 实现

```{python}
# 主应用程序界面
def main():
    st.set_page_config(
        page_title="AI 威士忌品鉴系统",
        page_icon="🥃",
        layout="wide"
    )

    # 侧边栏 Agent 选择
    with st.sidebar:
        st.header("🥃 威士忌 AI 品鉴")

        # Agent 选择
        agent_type = st.selectbox(
            "选择品鉴 Agent:",
            ["DrunkTony (中文)", "WhiskyFunny (English)", "WhiskyNotebook (English)"],
            help="每个 Agent 都有不同的性格和数据源"
        )

        # 模型选择
        model_options = get_model_options(agent_type)
        selected_model = st.selectbox("模型:", model_options)

    # 主内容区域
    st.header("专业的威士忌品鉴笔记生成器")

    # 输入部分
    col1, col2 = st.columns([2, 1])
    with col1:
        whisky_name = st.text_input(
            "输入威士忌名称:",
            placeholder="例如: Macallan 18 Year Old",
            help="输入完整的威士忌名称,包括年份和桶型"
        )

    with col2:
        st.write("")  # 间距
        generate_btn = st.button("🍷 生成品鉴笔记", type="primary")
        recommend_btn = st.button("🎯 获取推荐")

    # 输出部分
    if generate_btn:
        if not whisky_name:
            st.error("请输入威士忌名称")
        else:
            with st.spinner("正在分析威士忌..."):
                tasting_notes = generate_tasting_notes(whisky_name, agent_type, selected_model)
                st.markdown("### 🥃 品鉴笔记")
                st.markdown(tasting_notes)

    if recommend_btn:
        with st.spinner("正在查找推荐..."):
            recommendations = get_recommendations(tasting_notes, agent_type)
            st.markdown("### 🎯 个性化推荐")
            st.markdown(recommendations)
```



## 频率限制与成本管理

### API 使用控制

```{python}
# 频率限制实现
RATE_LIMIT_FILE = "rate_limit.json"
MAX_RUNS_PER_DAY = 80

def get_rate_limit_data():
    """获取当前使用数据"""
    try:
        with open(RATE_LIMIT_FILE, "r") as f:
            data = json.load(f)
            # 确保键存在
            if "date" not in data or "count" not in data:
                return {"date": str(datetime.date.today()), "count": 0}
            return data
    except (FileNotFoundError, json.JSONDecodeError):
        return {"date": str(datetime.date.today()), "count": 0}

def check_rate_limit():
    """检查用户是否超出了每日限制"""
    data = get_rate_limit_data()
    today = str(datetime.date.today())

    if data["date"] != today:
        # 为新的一天重置计数器
        return {"date": today, "count": 0, "can_run": True}

    if data["count"] >= MAX_RUNS_PER_DAY:
        return {"date": today, "count": data["count"], "can_run": False}

    return {"date": today, "count": data["count"], "can_run": True}
```

## 性能优化

### 向量数据库性能

```{python}
# 针对大数据的分批优化相似度搜索
def batch_similarity_search(query_embedding, db_path, table_name, batch_size=1000):
    """针对大型数据集的分批相似度搜索优化"""

    with duckdb.connect(database=db_path, read_only=True) as con:
        # 获取总行数
        total_rows = con.execute(f"SELECT COUNT(*) FROM {table_name}").fetchone()[0]

        all_similarities = []

        # 分批处理以管理内存
        for offset in range(0, total_rows, batch_size):
            batch_df = con.execute(
                f'SELECT full, bottle_embedding FROM {table_name} LIMIT {batch_size} OFFSET {offset}'
            ).fetchdf()

            # 计算该批次的相似度
            for _, row in batch_df.iterrows():
                embedding = np.array(row['bottle_embedding'])
                similarity = cosine_similarity(query_embedding, embedding)
                all_similarities.append((row['full'], similarity))

        # 排序并返回前几条结果
        all_similarities.sort(key=lambda x: x[1], reverse=True)
        return all_similarities[:10]
```

## 部署与配置

### 环境搭建

```bash
# .env 文件中的环境变量
SILICONFLOW_API_KEY=您的 SiliconFlow 密钥
MODELSCOPE_API_KEY=您的 ModelScope 密钥
GEMINI_API_KEY=您的 Gemini 密钥  # 可选

# 数据库初始化
python -c "
import duckdb
conn = duckdb.connect('data/whisky_database.duckdb')
# 创建支持向量的表格
"
```


### Shiny for Python 备选方案

```{python}
# 使用 Shiny 实现的备选 UI
from shiny import App, ui, render, reactive, req

app_ui = ui.page_fluid(
    ui.h2("🥃 AI 威士忌品鉴系统"),
    ui.layout_sidebar(
        ui.sidebar(
            ui.input_select("agent", "选择 Agent:", [
                "DrunkTony (中文)", "WhiskyFunny (English)", "WhiskyNotebook (English)"
            ]),
            ui.input_text("whisky_name", "威士忌名称:", ""),
            ui.input_action_button("generate", "生成品鉴笔记")
        ),
        ui.output_text_verbatim("tasting_notes"),
        ui.output_text_verbatim("recommendations")
    )
)

def server(input, output, session):
    @output
    @render.text
    def tasting_notes():
        req(input.generate())
        # 生成品鉴笔记逻辑
        return generate_tasting_notes(input.whisky_name(), input.agent())

app = App(app_ui, server)
```


## 技术成就

### 核心创新

1. **多 Agent 架构**: 拥有不同的 AI 人格和数据源。
2. **RAG 实现**: 使用真实威士忌评论进行检索增强生成。
3. **向量数据库**: 使用 376MB 数据库进行高效相似度搜索。
4. **双语支持**: 提供中文和英文 Agent。
5. **成本管理**: 内置频率限制和 API 优化。
6. **专业品鉴格式**: 符合行业标准的笔记结构。

### 性能指标

| 指标 | 数值 |
|--------|-------|
| 数据库大小 | 376MB (26,000+ 条评论) |
| 嵌入维度 | 1024 |
| 搜索延迟 | < 2 秒 |
| 每日请求限制 | 80 次请求 |
| 支持语言 | 2 (EN/ZH) |
| 数据源 | 3 (drinktony, whiskyfun, whiskynotes) |

## 未来增强方向

下一版本的潜在改进点:

1. **高级相似度算法**: 实现文本 + 元数据的混合搜索。
2. **用户个性化**: 随着时间推移学习用户的偏好。
3. **移动端应用**: 原生的 iOS/Android 应用程序。
4. **实时数据更新**: 自动化的网页抓取流水线。
5. **口味特征分析**: 根据用户偏好生成其口味特征图谱。
6. **社交功能**: 与社区分享品鉴笔记和推荐。

## 结论

这款威士忌品鉴应用展示了将 RAG 技术与专门的 AI Agent 相结合,从而构建复杂的领域特定系统的强大力量。本项目展示了:

- **先进的 AI 架构**: 具备不同人格的多 Agent 系统。
- **数据工程**: 大规模向量数据库管理。
- **专业知识**: 行业标准的品鉴笔记格式。
- **用户体验**: 跨平台的简洁、直观界面。
- **成本效益**: 智能的频率限制和优化。

无论您是威士忌爱好者、AI 开发人员还是数据科学家,本项目都为您提供了一个极佳的范例,展示了如何结合真实世界数据与先进机器学习技术来构建生产级 AI 应用。

---


**技术栈**: Python, Streamlit, DuckDB, OpenAI API, 向量嵌入 (Vector Embeddings)

**数据库**: 涵盖 3 个专门来源的 26,000+ 条真实威士忌评论。
 
 

本博客由 ❤️ 和 Quarto 构建。