CookGraph 是一个面向中文菜谱、食材和烹饪技法的知识图谱问答与推荐 Agent,基于 FastAPI + Vue + OpenAI 兼容本地/远端模型工具循环实现。
当前主链路是 V2 参数驱动架构:
flowchart TD
A["用户输入"] --> B["FastAPI + runtime memory"]
B --> C["query_understanding.classify_v2()"]
C --> D["QueryFrame JSON Schema"]
D --> E["entity_resolver.resolve()"]
E --> F["query_router._build_plan()"]
F --> G{"第一跳动作"}
G -->|recipe_query_tool| H["query_recipe_plan(plan)"]
G -->|web_search_tool| I["web_search_tool(query)"]
G -->|闲聊/歧义| J["直接回答/澄清"]
H --> K["V2 图谱 + plan 驱动向量召回"]
K --> L["统一 ToolResult JSON"]
I --> L
L --> M["结果策略 + 自然语言渲染"]
J --> N["SSE content"]
M --> N
N --> O["保存 assistant / rag_trace / token_usage"]
正向菜谱详情会在回答层继续按用途投影:ingredients 输出用料清单,full_recipe 输出用料、做法和火力时间;这些都是同一个 recipe_query_tool(plan) 的字段投影,不是额外工具。
- 直接回答烹饪、食材、菜谱、菜单相关问题。
- 使用
recipe_query_tool查询本地菜谱知识图谱(约 7.5 万节点、35.8 万条关系、13214 道菜),支持正向属性查询、反向关系查询、完整档案查询。 - Query Understanding:反向查询先经过结构化意图识别,区分食材、技法、口味、菜系;不确定时追问,不靠模型硬猜。
- 图谱节点名词召回:反向查询的向量化对象是图谱节点名词(如
牛肉、香辣味、川菜、蒸制),不是菜谱正文。 - 混合菜名召回:正向菜谱问题使用别名、字符 TF-IDF、
gte-large-zh向量和 RRF 融合,把“西红柿炒鸡蛋”归一到图谱标准菜名。 - 精确菜名保护:如果用户问题已经包含图谱标准菜名(如“小炒黄牛肉”),跳过语义改写,直接查图谱,避免别名误替换。
- 唯一意图入口:
query_understanding.classify_v2()输出经过 JSON Schema 校验的QueryFrame;模型失败时澄清,不恢复旧语义正则。 - 结构化路由:
query_router()将归一化后的QueryFrame转成recipe_query_tool(plan)或web_search_tool(query)。 - 统一工具结果:两个工具都返回 JSON 兼容的
ToolResult,前端只显示自然语言,不显示内部 JSON。 - Grounded Answer:最终回答以工具结果为最高优先级,不允许工具未命中时编造菜名、做法或用量。
- Clarification Gate:多类型歧义词(如"蒜蓉"可能指辅料或技法)返回结构化追问,不走模型硬猜。
- 使用
web_search_tool联网搜索公开网页资料(本地图谱未命中时自动兜底)。 - 可自动通过 SSH 隧道连接远端 LM Studio 的 OpenAI 兼容 API。
- 对话持久化:后端重启后用同一个
session_id恢复对话历史和菜谱上下文。 - 偏好记忆:跨会话记住用户偏好(通过 SQLite 持久化)。
- Token 用量追踪:实时估算和记录每轮对话的 tokens 消耗。
CookGraph/
├── backend/
│ ├── app.py # FastAPI 主应用
│ ├── context_manager.py # 对话上下文组装(Zleap 风格)
│ ├── agent_adapter_local_LLM_harness.py # 推荐适配器(前置查询路由 + 工具循环)
│ ├── agent_adapter_local_LLM.py # 本地 vLLM 基础版
│ ├── agent_adapter.py # DeepSeek API 适配器
│ ├── agent_tools.py # 工具定义(@tool 装饰器)
│ ├── tool_calling.py # 工具调用解析/执行/trace
│ ├── recipe_query_adapter.py # V2 plan 执行适配器
│ ├── recipe_query_v2.py # 参数驱动图谱查询引擎
│ ├── query_understanding.py # 唯一 QueryFrame 意图入口
│ ├── entity_resolver.py # 实体归一化
│ ├── tool_result.py # 统一 JSON 工具结果协议
│ ├── tool_result_policy.py # 工具证据与联网策略
│ ├── answer_composer.py # 查询结果格式化
│ ├── clarification_gate.py # 歧义追问门控
│ ├── recipe_semantic_retriever.py # 菜名 embedding 基础能力
│ ├── recipe_recommendation_vector_retriever.py # 推荐向量召回
│ ├── recipe_relation_vector_retriever.py # 关系字段向量召回
│ ├── token_usage_tracker.py # Token 用量估算
│ ├── memory_store.py # 会话内存缓存 + SQLite 持久化
│ ├── chat_persistence.py # SQLite 读写层(chat_sessions / chat_messages)
│ ├── preference_memory.py # 用户偏好记忆存储
│ ├── session_recipe_context.py # 当前会话菜谱上下文管理
│ ├── schemas.py # API / SSE 数据结构
│ └── llm_endpoint.py # 模型端点探活与重试
├── config/
│ ├── 2kg_chem+recipe_fire_12K.pkl # 本地准备的知识图谱,不随公开仓库发布
│ ├── chem+recipe_kg_updated_fire.pkl # 小图备份(50 道火力增强菜)
│ ├── build_stats.json # 图谱构建统计
│ ├── recepi/ # 实体/关系/属性配置文件
│ ├── recipe_aliases.json # 菜名同义词典
│ ├── recommendation_aliases.json # 推荐场景别名表
│ ├── entity_ambiguities.json # 需要用户确认的泛称实体
│ └── reverse_entity_aliases.json # 反向查询实体归并配置
├── frontend/
│ ├── src/
│ │ ├── components/Chat/
│ │ │ ├── ChoicePromptCard.vue # 歧义追问选择卡片
│ │ │ └── TokenUsageBadge.vue # Token 用量显示
│ │ ├── stores/
│ │ │ └── chat.ts # 聊天状态 + SSE 解析
│ │ └── types/chat.ts # 消息类型定义
│ └── package.json
├── test/
│ ├── recipe_test_data.py # 150 条单轮测试用例数据
│ ├── run_recall_test.py # 召回率测试运行器(可联网兜底)
│ ├── multiturn_test_data.py # 20 个多轮对话测试 case
│ ├── run_multiturn_dialogue_test.py # 多轮对话测试运行器(真实 agent 链路)
│ ├── run_all_tests.py # 全量测试入口
│ ├── test_query_understanding.py # Query Understanding 单元测试
│ ├── test_query_router.py # V2 路由与 plan 契约测试
│ ├── test_answer_composer.py # 答案格式化单元测试
│ ├── test_clarification_gate.py # 歧义追问单元测试
│ ├── test_chat_persistence.py # 对话持久化单元测试
│ ├── test_zleap_lite_memory.py # Zleap-lite 记忆系统测试
│ ├── test_recipe_query_adapter_guardrails.py # 适配器防护测试
│ ├── test_recipe_routing_regressions.py # 路由回归测试
│ ├── test_recommendation_vector_retriever.py # 推荐向量测试
│ ├── test_recommendation_aliases.py # 推荐别名配置测试
│ ├── test_relation_vector_trigger.py # 关系向量字段测试
│ ├── test_tool_input_contract.py # 工具入参契约测试
│ └── test_tool_routing_guardrails.py # 工具路由防护测试
├── doc/
│ ├── USER_MESSAGE_CALL_CHAIN.md # 完整调用链文档(含 3 张流程图)
│ ├── query_understanding_refactor_plan.md # Query Understanding 重构方案
│ ├── zleap_lite_chat_persistence_plan.md # 持久化设计方案
│ └── memory_zleap_lite_plan.md # 记忆系统设计
├── Dockerfile # 依赖环境 Docker 镜像
├── docker/
│ └── docker-entrypoint.sh
├── deploy_uv.sh # uv 一键部署脚本
├── start_docker.sh # Docker 一键构建并启动脚本
├── .env.example
├── requirements.txt
├── scripts/ # 离线构建推荐别名和向量索引
└── start.py
推荐使用 uv 部署脚本。它会安装后端依赖、前端依赖,并默认下载 gte-large-zh embedding 模型到 models/gte-large-zh:
bash deploy_uv.sh常用选项:
# 跳过模型下载
bash deploy_uv.sh --skip-model
# 跳过前端依赖,只安装后端依赖并下载 embedding 模型
bash deploy_uv.sh --skip-frontend
# 部署完成后直接启动
bash deploy_uv.sh --start
# 网络或 CI 环境下顺序安装,便于定位失败日志
bash deploy_uv.sh --no-parallel脚本默认优先使用本机 Python;缺 Python 时 uv 会按 UV_PYTHON_INSTALL_MIRROR 下载解释器。Python/npm 包索引默认使用官方源,可按网络情况切镜像:
开放式推荐问题(例如“我有辣椒和牛肉,可以做什么菜”“今天天气热适合吃什么菜”)使用本地推荐向量索引。这个索引不在用户请求时全量构建,需要显式离线生成:
python scripts/build_recommendation_aliases.py
python scripts/build_recommendation_vector_index.py生成文件:
config/recommendation_aliases.jsonconfig/recommendation_aliases.rejected.jsonbackend/.cache/recipe_recommendation_vector_index.npz
第一版只推荐本地 config/2kg_chem+recipe_fire_12K.pkl 已收录菜品,不联网补菜,不新增 Agent 工具。运行时仍通过 recipe_query_tool 内部完成推荐。
UV_INDEX_URL=
NPM_REGISTRY=https://registry.npmmirror.com
MODEL_SOURCE=modelscope
MODELSCOPE_MODEL_ID=AI-ModelScope/gte-large-zh
UV_PYTHON_INSTALL_MIRROR=https://registry.npmmirror.com/-/binary/python-build-standalone/
UV_CONCURRENT_DOWNLOADS=8
UV_CONCURRENT_BUILDS=4如果 HuggingFace 镜像报 SSL: UNEXPECTED_EOF_WHILE_READING,优先使用默认的 MODEL_SOURCE=modelscope。
公开仓库不包含约 51 MB 的 config/2kg_chem+recipe_fire_12K.pkl。启动前请将知识图谱文件放置到以下路径:
CookGraph/config/2kg_chem+recipe_fire_12K.pkl
如果没有该文件,后端仍可启动,但本地菜谱查询和推荐功能会返回“知识图谱文件不存在”。该文件属于运行数据,不应提交到公开 Git 仓库;构建推荐别名和向量索引时也需要先准备该文件。
Windows 建议:
- 推荐在 Git Bash 里运行
bash deploy_uv.sh。 - 如果系统里的
bash是C:\Windows\System32\bash.exe,它会进入 WSL。不要用 WSL bash 混跑 Windows.venv。
项目提供 Docker 一键启动脚本。镜像会在构建时安装 Python/前端依赖,并下载 gte-large-zh embedding 模型。项目源码和 .env 通过 volume 从本机读取。
start_docker.sh 只支持在 Ubuntu / WSL / Linux shell 内运行。Windows 用户请先进入 WSL/Ubuntu。
cd /mnt/e/CookGraph
bash start_docker.sh常用选项:
# 强制重新构建镜像
bash start_docker.sh --rebuild
# 改宿主机端口
bash start_docker.sh --backend-port 18000 --frontend-port 15173
# 切换模型下载来源
MODEL_SOURCE=huggingface bash start_docker.sh --rebuildPowerShell:
cd E:\CookGraph
.\.venv\Scripts\python.exe start.pyGit Bash:
cd /e/CookGraph
.venv/Scripts/python.exe start.py默认会启动:
- 后端:
http://localhost:8000 - 前端:
http://localhost:5173 - 默认适配器:
agent_adapter_local_LLM_harness
调试大模型返回值:
.venv/Scripts/python.exe start.py --debug-llm.env 里已经按本地模型模式配置:
LLM_MODEL=qwen3-4b
LLM_BASE_URL=http://127.0.0.1:51234/v1
LLM_API_KEY=not-needed
LLM_MAX_TOKENS=2048
LLM_NO_THINK=1
MAX_MODEL_LEN=32768
MAX_TOOL_TURNS=5
MAX_TOTAL_TOOL_CALLS=5
MAX_CONSECUTIVE_TOOL_CALLS=3如果远端 LM Studio 只监听 127.0.0.1:1234,可以开启 SSH 隧道:
LLM_SSH_TUNNEL=0
# LLM_REMOTE_HOST=your.server.com
# LLM_REMOTE_USER=ubuntu
# LLM_REMOTE_PASSWORD=your_password
# LLM_REMOTE_PORT=1234
# LLM_LOCAL_PORT=51234推荐适配器是 backend/agent_adapter_local_LLM_harness.py。它把运行时实际注册的工具列表塞进中文系统提示词,并让模型通过结构化 tool_call 调用:
stream_search_agent() 先调用 route_query(user_text, history),由结构化 QueryFrame 统一决定第一跳动作:直接回答、澄清、recipe_query_tool 或 web_search_tool。
当前真实顺序是:
- 图谱统计:如“本地收录多少道菜”,直接调用
recipe_query_tool。 - 上下文属性追问:如果历史里有当前菜品,用户只问“火力呢”“需要哪些调料”“注意事项”,会补全成“当前菜品 + 属性”后调用
recipe_query_tool。 - Clarification Gate:
decide_clarification()处理明确联网、已知菜名、缺菜名属性问题、疑似错字菜名、口味+食材的推荐/单菜歧义。 - 意图分类:
classify_v2()输出 JSON Schema 校验后的 QueryFrame;模型失败时澄清,不恢复旧语义正则。
第一跳工具由 query_router 决定,模型不再负责选择第一跳工具:
recipe_query_tool(plan):只接收结构化 plan,执行 V2 图谱查询和 plan 驱动的向量召回。web_search_tool(query):返回统一 JSON 结果;只有单菜谱未命中且允许联网时自动调用。
反向查询(如“哪些菜用了牛肉”“有哪些川菜”)由 QueryFrame 的 reverse_entity_query plan 驱动图谱节点和边关系查询(USES_MAIN_INGREDIENT / USES_TECHNIQUE / HAS_TASTE / BELONGS_TO_CUISINE)。
工具执行完成后,_emit_final_answer_from_tool_context() 按以下优先级生成回答:
- 结构化工具结果 →
_build_json_grounded_answer() - 联网结果 →
_build_grounded_web_fallback_answer() - 联网提议 →
_build_grounded_web_search_offer_answer() - 终止失败摘要 →
render_terminal_recipe_failure() - 反向/正向菜谱结果 → 对应 grounded answer
- 以上都不满足 → content-only 模型
_stream_model_answer()
前五类优先直接生成用户可读文本,不把内部 JSON 交给用户;只有没有可靠工具证据时才调用 content-only 模型。
MAX_TOOL_TURNS:最多模型工具回合数(默认 5)。MAX_TOTAL_TOOL_CALLS:本轮总工具调用上限(默认 5)。MAX_CONSECUTIVE_TOOL_CALLS:同一个工具最多连续调用次数(默认 3)。
项目实现了轻量级的 Zleap-lite 记忆系统:
- 会话菜谱上下文:当前会话的最近菜品、查询、菜谱摘要,注入到每轮 prompt 中,支持"它蒸多久""刚才那道菜"等指代追问。
- 用户偏好记忆:通过 SQLite 跨会话保存用户偏好(如口味偏好、常用食材)。
- 对话持久化:后端重启后用同一个
session_id恢复完整对话历史、trace 和菜谱上下文。
| 表 | 用途 |
|---|---|
chat_sessions |
会话元信息 + 菜谱上下文快照 |
chat_messages |
顺序消息 + assistant 的 rag_trace_json(含 token_usage) |
写入路径同时写内存缓存和 SQLite;get_session() 先查内存,未命中则从 SQLite hydrate。
python -m compileall backend start.pyPYTHONIOENCODING=utf-8 .venv/Scripts/python.exe test/run_all_tests.py| 测试类型 | 运行命令 | 用例数 | 覆盖 |
|---|---|---|---|
| 单轮召回率 | python test/run_recall_test.py --phase all |
150 条 | 正向/反向/模糊/边界 + 联网兜底 |
| Pytest 单元回归 | conda run -n bigdog python -m pytest -q |
当前 93 项 | 路由、plan、工具协议、推荐、持久化、答案渲染 |
| 多轮对话 | python test/run_multiturn_dialogue_test.py --all |
20 个 case | 记忆/抗干扰/逻辑自洽 + DeepSeek LLM 裁判 |
| 持久化 | python test/test_chat_persistence.py |
6 项 | SQLite round-trip / hydrate / archive |
| Zleap-lite 记忆 | python test/test_zleap_lite_memory.py |
9 项 | 偏好记忆 + 菜谱上下文渲染 |
| Query Understanding | python -m unittest test.test_query_understanding |
12 项 | JSON、属性字段、上下文和意图合同 |
| V2 路由与 plan | python -m pytest test/test_query_router.py |
— | QueryFrame 到 plan 的契约测试 |
| 答案格式化 | python -m unittest test.test_answer_composer |
— | 结果格式化测试 |
| 歧义追问 | python -m unittest test.test_clarification_gate |
— | Clarification Gate 测试 |
测试 agent 行为的三大能力:
# 全部类别(需远端 LLM + DeepSeek 裁判)
PYTHONIOENCODING=utf-8 python test/run_multiturn_dialogue_test.py --all
# 单独跑某一类
python test/run_multiturn_dialogue_test.py --category memory
python test/run_multiturn_dialogue_test.py --category distraction
python test/run_multiturn_dialogue_test.py --category contradiction多轮测试使用真实 agent 链路(stream_search_agent),不走 mock。DeepSeek 作为 LLM 裁判,输出结构化 JSON 判定每个 case 是否通过。配置 DEEPSEEK_API_KEY 环境变量启用裁判。
test/.artifacts/test_results.json— 单轮测试详细结果test/.artifacts/test_report.md— 单轮测试报告test/.artifacts/multiturn_test_results.json— 多轮测试详细结果test/.artifacts/multiturn_test_report.md— 多轮测试报告
默认菜谱知识图谱路径是 config/2kg_chem+recipe_fire_12K.pkl(约 51MB,需单独准备),包含 75242 个节点、358690 条关系、13214 道菜。其中 chem+recipe_kg_updated_fire.pkl 是小图备份,包含 50 道带 fire_control_process 的火力增强菜;大图已经完整包含小图节点、边和火力字段。
图谱通过 pickle 反序列化为 networkx.DiGraph,查询层基于:
dish_nodes:菜名到节点 ID 的索引。all_nodes_by_label:实体类型到节点名的索引。graph.edges()/graph.in_edges():正向和反向关系遍历。
完整调用链见 doc/USER_MESSAGE_CALL_CHAIN.md,包含当前 V2 的 3 张 Mermaid 流程图、会话恢复、JSON 工具协议、属性投影、上下文注入、联网降级和 SSE 渲染说明。