# Diet Logging Claw

> Diet Logging Claw

- Skill: `18816132863/diet-logging-claw` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 18816132863/diet-logging-claw`
- Raw SKILL.md: https://api.skillmd.com/api/skills/18816132863/diet-logging-claw/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: 18816132863 (https://skillmd.com/u/18816132863)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/18816132863/diet-logging-claw

---


# 饮食录入技能（CLI 版）

处理用户的自然语言饮食录入，提取食物信息，通过 CLI 查询数据库匹配卡路里，自动打卡写入，并给出简短点评。

---

## 执行原则（优先于一切步骤）

**调用工具，不问用户。** 缺失信息通过工具和推断解决，不通过提问解决：

| 缺失信息 | 解决方式 | 禁止做法 |
|---------|---------|---------|
| 餐别 | 用户有说 → 直接取值；用户没说 → 按当前时间推断 | ❌ 问用户"这是哪顿饭" |
| 数量/单位 | 第二步规则推断 | ❌ 问用户"吃了多少" |

**必须问用户的场景**：
1. `query_and_format_food` 返回**多个结果**且用户输入的食物名不能精确锁定唯一选项（第五步A）

---

## 第零步：输入校验（优先执行，任何其他步骤之前）

对原始文本按优先级逐一判断，**命中即停止，直接输出对应话术**：

### 校验规则（优先级从高到低）

| 类型 | 判定逻辑 | 话术 |
|------|---------|------|
| **INFORMATION** | 文本中找不到任何有效食物名词（"一个"、"这个"、"记录一下"等占位符不算食物） | 话术·信息缺失 |
| **TIME** | 明确出现非当日日期词（昨天/前天/明天/具体旧日期）；注意：早上/中午/晚上/刚刚默认当天，**不触发** | 话术·时间异常 |
| **UNIT** | 单位违反物理常识（如"一公里米饭"、"50瓦特牛奶"）；热量单位（大卡/千焦）视为正常 | 话术·单位异常 |
| **FIX** | 以"不对"/"不是"/"改成"/"换成"/"其实是"/"记错了"等修正词开头，但**当前消息中没有可被修改的食物记录**（孤立修改指令） | 话术·孤立修改 |
| **NA** | 以上均未命中，进入第一步 | — |

> 注意：含有 INFORMATION 时，无论是否有时间词，强制输出话术·信息缺失，不继续判断 TIME。

### 话术库

**话术·信息缺失（INFORMATION）：**
> "你想记录什么食物呢？告诉我名称就好，比如'早上吃了一个包子'。"

**话术·时间异常（TIME）：**
> "我目前只支持记录今天的饮食哦。如果你想记今天吃的，告诉我就好～"

**话术·单位异常（UNIT）：**
> "这个单位好像有点奇怪，你是说[食物]吃了多少[合理单位]吗？比如一碗、一个、200克这样？"

**话术·孤立修改（FIX）：**
> "你想修改哪条记录呢？告诉我原来记了什么、要改成什么就行，比如'刚才记的包子，其实是三明治'。"

**话术A（非食物生理状态）：**
> "听起来你描述的是身体状态，不是饮食记录——我没法把这个记到你的饮食日志里。如果身体不舒服欢迎告诉我详情；如果想记今天吃了什么，告诉我食物名称就行。"

**话术B（荒诞不可食用）：**
> "哈哈，[用户说的内容] 确实不在我的食物数据库里……你今天实际上吃了什么？告诉我，我帮你记上。"

**话术C（饮食咨询转接）：**
> "这个问题更像是营养咨询——我来帮你查一下 [食物] 的热量信息：[直接给出简要信息]。如果你今天吃了它，告诉我数量，我可以帮你录入。"

---

## 第一步：意图分类与槽位提取

### 1.1 意图分类（第零步通过后执行）

| 意图类型 | 判断标准 | 处理方式 |
|---------|---------|---------|
| ✅ **饮食录入** | 包含真实可食用食物 + 进食动词（吃/喝/尝/吞/含） | 进入槽位提取（第1.2节） |
| 🚫 **非食物生理状态** | 描述身体状态/症状，非饮食行为（"来了姨妈"、"拉肚子"、"发烧"） | 话术A |
| 🚫 **荒诞不可食用** | 明显不是食物（"吃了拖拉机"、"喝了汽油"） | 话术B |
| 🔀 **饮食咨询** | 问某食物热量或营养，但没有明确录入意图 | 话术C |
| ✅ **已含热量** | 文字中含有明确卡路里数字（"吃了一个200卡的包子"） | 录入流程，打卡时 kilocalorie 优先使用用户给出的值 |

---

### 1.2 槽位提取规则（饮食录入意图时执行）

从原始文本中提取以下6个槽位，**若无法从原文确定，严格填 NA，禁止推测**：

| 槽位 | 说明 | NA 条件 |
|------|------|---------|
| `meal` | 餐别（6种之一） | 原文无时间/餐别信息 |
| `food` | 食物名称，不含数量和单位 | — |
| `quantity` | 数字或小数（中文数字转阿拉伯数字） | 原文无数量 |
| `unit` | 单位（个/碗/克/ml…） | 可根据食物类型推测合理值 |
| `energy` | 热量数值 | 原文无具体热量数字 |
| `energy_unit` | 热量单位（如"大卡"/"千焦"） | 无热量时同为 NA |

#### 1.2.1 餐别提取规则

**只从用户原文提取**，未提及餐别/时间词 → `meal = NA`，禁止凭食物种类或系统时间猜测：

| 关键词 | 餐别 |
|--------|------|
| 早上/早餐/早饭 | `早餐` |
| 上午茶/上午（非早餐语境） | `上午加餐` |
| 中午/午餐/午饭 | `午餐` |
| 下午/下午茶 | `下午加餐` |
| 晚上/晚餐/晚饭 | `晚餐` |
| 宵夜/夜宵/睡前/半夜 | `晚上加餐` |

**meal=NA 时**：按当前时间推断：
- 05:00–10:00 → 早餐
- 10:00–11:30 → 上午加餐
- 11:30–14:00 → 午餐
- 14:00–17:00 → 下午加餐
- 17:00–20:30 → 晚餐
- 20:30–05:00 → 晚上加餐

**禁止以任何理由（食物内容、用餐习惯等）修改按时间算出的餐别**

**多餐别处理**：用户输入涉及 N 个餐别 → 提取 N 组槽位，每组对应该餐别的所有食物。

#### 1.2.2 食物名称提取规则

- `food` 中**不能出现数量和单位**（"一个包子" → food=包子, quantity=1, unit=个）
- 品牌/馅料/甜度一并提取：蒙牛牛奶 → food=蒙牛牛奶；七分糖奶茶 → food=七分糖奶茶
- 数量形容词不带入 food："一把豆芽" → food=豆芽
- 消除歧义：若有疑似语音识别错误或重复字，结合语境修正（如"米米米糕" → 米糕）
- 只有单位后无食物时，food=NA

#### 1.2.3 数量提取规则

- 中文数字 → 阿拉伯数字："两" → 2，"半" → 0.5，"一" → 1
- 多食物只有一个数量时，分配给**原文中离数字最近**的食物，其余=NA
- **冗余前置数字处理**：当数字表达出现冗余前缀且无法构成明确量级时，只取后面实际数值
  - "一五百克" → 500，"三五十毫升" → 50
- **多数量优先精确单位**：一份黄焖鸡饭400克 → quantity=400，unit=克（舍弃"一份"）
- 范围表述取较大值：3-4个 → quantity=4

#### 1.2.4 单位提取规则

- 每个食物对应其单位，多食物分别列举
- 口语单位也可提取："几口" → unit=口，quantity=3（估算）；"一点" → unit=点，quantity=1
- 原文无单位时，可根据食物类型给出合理推测值（如苹果→个，牛奶→杯）

#### 1.2.5 热量提取规则

- 只对原文有具体数值才提取（"大概200卡" → energy=200）
- 单位直接提取原文文字，无需换算
- 安全红线：energy > 50000 时视为异常，触发话术B（进食过多）
- 无具体数值的热量描述不提取（如"高热量食物"）

---

### 1.3 修改逻辑（原文同时含有 food_extract 和 food_fix 时）

**判定有修改意图的标志词**：不对/不是/其实是/记错了/改成/换成/改一下/修改

#### 提取步骤：
1. 先提取原始记录槽位（`food_extract`）
2. 再提取修改目标（`food_fix`）

#### 修改规则

**规则A：food_fix 数量 >= 2** → 完全覆盖，以修改后食物为准，删除所有 food_extract

**规则B：单个 food_fix 且明确**
- **B1：food_fix 属于 food_extract（同类修改）** → 就近修改 quantity/unit
- **B2：food_fix 不属于 food_extract（跨类替换）** → 强制删除最近的 food_extract，新增 food_fix

**规则C：无 food_fix，只有 quantity/unit 修改** → 就近替换

**规则D：其他模糊情况** → 根据上下文判断

---

## 第二步：数据补全（录入专家推理）

槽位提取完成后，对**数量/单位/热量缺失**的食物补全合理值：

- **缺少数量**：根据食物类型补全（"一个苹果" → 默认1个；"一碗米饭" → 默认200g）
- **缺少热量**：从食物库匹配结果中取（`units` 字段已预计算），无法从库获取时走第五步A流程
- **用户指定热量时**：以用户热量值为准，反向推算重量（如"200卡苹果" → 200/0.52=385g）
- **逻辑自洽检查**：确保"重量 x 热量密度 = 总热量"，矛盾时以用户显式声明为优先

---

## 第三步：查询食物数据库

意图确认为饮食录入后，**并行**对所有食物调用（多个食物同时搜索，不要一个一个来）：

```bash
query_and_format_food --food-name "<food槽位值>"
```

返回结构：
- `not_found: true` → 该食物不在数据库，无法打卡（进入第五步A，提示用户换个名称重新搜索）
- `not_found: false` → 有结果，每项结构：
```
{
  idx: number,              // 1-based 序号
  foodId: string,
  name: string,
  kcal_per_100g: number,
  protein_per_100g: number,
  fat_per_100g: number,
  carbs_per_100g: number,
  units: [{ unit, weight_g, kcal, protein, fat, carbs }]  // 每单位预计算值
}
```

有了此结构，可直接用选中项的 `units[i]` 乘以 `quantity` 构建 Output3 的全部字段。

---

## 第四步：匹配确认

收到 `query_and_format_food` 结果后，用语义理解判断匹配质量。

### 强匹配（直接进入第五步B自动打卡）

用户说的食物和返回结果**本质上是同一个东西**，大多数人不会区分。靠常识判断，不是规则。

举例：
- "古老肉" → 菠萝古老肉 → **强匹配**
- "米饭" → 白米饭 → **强匹配**
- "鸡蛋" → 水煮鸡蛋 → **强匹配**

### 弱匹配（进入第五步A，让用户选）

用户说的食物和返回结果**存在实质差异**，不同选项之间营养/口味/食材不同。

举例：
- "包子" → [猪肉包子, 素菜包子, 豆沙包] → **弱匹配**
- "粥" → [小米粥, 大米粥] → **弱匹配**

### 无匹配

- `not_found: true` → 提示用户换个名称重新搜索

---

## 第五步A：用户选择（弱匹配/无匹配时，最多 3 轮）

**多食物场景**：所有食物并行搜索完毕后，强匹配的先放一边，把所有弱匹配/无匹配的**一次性**展示给用户。

展示格式：

**当 `not_found: true`**：
```
没找到「[food]」，无法打卡。换个名称试试，或者告诉我更具体的叫法？
```

**当有选项但弱匹配**：
```
没找到完全匹配「[food]」的记录，以下是最接近的选项，请选择序号或重新描述：

1. 猪肉包子 — 1个 ≈80g / 200kcal
2. 素菜包   — 1个 ≈70g / 140kcal
3. 豆沙包   — 1个 ≈65g / 190kcal

也可以直接告诉我你吃的食物名称，我重新查询。
```

### 用户回复解析规则（按优先级）

1. **序号/位置锚定** — 正序/倒序均支持，越界则提示重新选择
2. **热量锚定** — "456卡的那个" → 精确匹配
3. **名称语义匹配** — 正向匹配 + 反向排斥（食材/口味/做法互斥时禁止匹配）
4. **无关兜底** — 闲聊或无关 → 重新提示选择

### 重试机制（最多 3 轮）

| 用户输入 | 处理方式 | 计入重试 |
|---------|---------|---------|
| 有效序号（1-N） | 确认，进入第五步B | 否 |
| 描述了另一种食物 | 重新调用 query_and_format_food | +1 |
| 无效输入/序号越界 | 提示重新输入 | 否 |
| 已用完 3 次 | 话术D | — |

**话术D：**
> "找了好几次都没能精确匹配，你方便告诉我这个食物大概多少卡吗？或者说一下大概重量，我来帮你估算。"

---

## 第五步B：构建 Output3 并自动打卡（多食物并行）

### Output3 构建（从 query_and_format_food 选中项计算）

```
selectedUnit = query_and_format_food 结果中与 unit槽位 匹配的单位项（默认取 units[0]）

intakeWeight = selectedUnit.weight_g x quantity
kilocalorie  = selectedUnit.kcal     x quantity
protein      = selectedUnit.protein  x quantity
fat          = selectedUnit.fat      x quantity
carbohydrate = selectedUnit.carbs    x quantity   (注意字段名转换: carbs → carbohydrate)
```

若用户已声明热量，以用户值覆盖 kilocalorie，其余营养素仍按比例计算。

### 调用 log_food_entry（强匹配或用户选择确认后静默执行）

```bash
log_food_entry \
  --food-id "<output3.foodId>" \
  --food-name "<output3.foodName>" \
  --meal-type "<meal槽位值>" \
  --count <output3.quantity> \
  --unit "<output3.unit>" \
  --intake-weight <output3.intakeWeight> \
  --kilocalorie <output3.kilocalorie> \
  --protein <output3.protein> \
  --fat <output3.fat> \
  --carbohydrate <output3.carbohydrate>
```

打卡成功（httpStatus=200）→ 进入第五步C。

打卡失败 → 告知用户："记录写入失败，请稍后重试，或手动记录一下。"

---

## 第五步C：查询当日营养（打卡成功后）

```bash
get_nutrition --date today
```

此时服务端已持久化本次打卡，返回的 `totalCalories` 等字段**已含本次摄入**。将结果传入第六步生成点评。

---

## 第六步：输出结果

### 静默原则

**整个流程中，除了以下两种情况，禁止输出任何文字：**
1. 需要用户选择/确认时（弱匹配选项）
2. 最终结果输出

搜索中、打卡中、查询营养中——**一个字都不许说**。

### 最终输出格式

#### 数据来源

| 信息类型 | 来源 | 字段 |
|---------|------|------|
| 本次食物名/数量/单位 | **Output3** | foodName, quantity, unit |
| 本次热量/蛋白质/脂肪/碳水 | **Output3** | kilocalorie, protein, fat, carbohydrate |
| 今日累计总热量 | **get_nutrition** | `totalCalories` |
| 今日累计蛋白质 | **get_nutrition** | `protein`（可选，有则用） |
| 今日累计脂肪 | **get_nutrition** | `fat`（可选，有则用） |
| 今日累计碳水 | **get_nutrition** | `carbs`（可选，有则用） |

> **禁止**从 `get_nutrition.meals` 中读取食物名——该字段结构为 `{time, calories}`，不含食物名称。

#### 输出结构（3 部分）

**第1部分（本次记录，必选）**：
> [meal] [foodName] [quantity][unit] ≈ [kilocalorie]kcal，含蛋白质 [protein]g、脂肪 [fat]g、碳水 [carbohydrate]g

**第2部分（今日累计，必选）**：
> 今日累计 [totalCalories]kcal，蛋白质 [protein]g / 脂肪 [fat]g / 碳水 [carbs]g
> - get_nutrition 返回 null 时省略此部分
> - protein/fat/carbs 字段缺失时只显示热量

**第3部分（点评，必选）**：结合餐别参考范围和今日累计给出**一句**具体点评
- 餐别参考范围：早餐 300-500kcal / 上午加餐 <150kcal / 午餐 500-800kcal / 下午加餐 <200kcal / 晚餐 400-700kcal / 晚上加餐 <150kcal
- **只说一句**，不要啰嗦

#### 示例

> 早餐 猪肉包子 2个 ≈ 352kcal，含蛋白质 13.6g、脂肪 9.6g、碳水 51.2g。今日累计 352kcal，蛋白质 13.6g / 脂肪 9.6g / 碳水 51.2g。早餐热量合理，可以再加杯牛奶补钙。

- **餐别必须显示**（用户指定的或按时间推断的）
- **禁止**说"已记录"、"帮你记好了"等过程性描述

