技术博客

← 全部文章

Personal Memory Engine:Supabase RLS + FastAPI 实现详解

Engineering System ADR-001 定义了架构。Invest AI ADR-031 定义了 API 接口。这篇文章记录我们具体怎么实现的。

Schema 设计

Supabase 中一张表,延续现有的 user_settings 模式:

create table if not exists public.user_memory (
    id                bigint generated by default as identity primary key,
    user_id           uuid not null unique references auth.users (id) on delete cascade,
    age               integer,
    goals             text[]   not null default '{}',
    constraints       text[]   not null default '{}',
    health_conditions text[]   not null default '{}',
    financial_profile jsonb    not null default '{}',
    family_members    jsonb[]  not null default '{}',
    preferences       jsonb    not null default '{}',
    decision_style    text check (decision_style in ('analytical', 'intuitive', 'collaborative')),
    language          text not null default 'en' check (language in ('en', 'zh', 'ko')),
    version           integer  not null default 1,
    created_at        timestamptz not null default timezone('utc', now()),
    updated_at        timestamptz not null default timezone('utc', now())
);

user_id 上的 UNIQUE 约束意味着 upsert 只需简单的 on_conflict=user_id——不需要额外的查找。

行级安全策略

四条策略,每个操作一条。模式与 user_settings 完全相同:

alter table public.user_memory enable row level security;

create policy "users can read own memory"
on public.user_memory for select
using (auth.uid() = user_id);

create policy "users can insert own memory"
on public.user_memory for insert
with check (auth.uid() = user_id);

create policy "users can update own memory"
on public.user_memory for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);

create policy "users can delete own memory"
on public.user_memory for delete
using (auth.uid() = user_id);

为什么用四条策略而不是一条 ALL 策略?更明确。每个操作的意图一目了然。未来添加管理员读取策略(用于支持工具)时,不会意外授予管理员写权限。

MemoryService —— httpx 模式

该服务遵循与 SettingsService 完全相同的模式——没有新的抽象:

class MemoryService:
    async def get_memory(self, user_id: str) -> dict[str, Any]:
        supabase = get_supabase_config(self._config)
        headers = build_rest_headers(service_role=True, config=supabase)
        url = build_rest_url(
            f"rest/v1/user_memory?select=*&user_id=eq.{user_id}&limit=1",
            config=supabase,
        )
        async with httpx.AsyncClient(timeout=10) as client:
            response = await client.get(url, headers=headers)
            response.raise_for_status()
            rows = response.json()
        return _parse_row(rows[0]) if rows else _empty_memory(user_id)

service_role=True 在后端绕过 RLS——JWT auth 在 FastAPI 层(get_current_user)强制执行,不在 Supabase 层重复。这与项目中所有其他 Supabase 服务的工作方式一致。

LLM Context Block

关键方法是 build_llm_context_block:

async def build_llm_context_block(self, user_id: str) -> str:
    memory = await self.get_memory(user_id)
    lines = ["## 用户上下文(个人记忆)"]
    if memory.get("age"):
        lines.append(f"- 年龄:{memory['age']}")
    if memory.get("goals"):
        lines.append(f"- 目标:{', '.join(memory['goals'])}")
    # ... health_conditions, financial_profile, family_members ...
    return "\n".join(lines)

这个字符串被添加到 worker 的 LLM system prompt 前面。每日简报 worker 现在知道用户 55 岁、保守型、正在为女儿的医学院做规划——无需任何 UI 变更。

API 路由

app.py 中三条路由,全部需要 get_current_user:

@api_v1.get("/memory")
async def get_memory(current_user=Depends(get_current_user), ...):
    return await memory_service.get_memory(current_user["user_id"])

@api_v1.put("/memory")
async def update_memory(payload: dict, current_user=Depends(get_current_user), ...):
    return await memory_service.upsert_memory(current_user["user_id"], payload)

@api_v1.delete("/memory", status_code=204)
async def delete_memory(current_user=Depends(get_current_user), ...):
    await memory_service.delete_memory(current_user["user_id"])

DELETE 返回 204(无内容)。这是 GDPR 删除接口——硬删除,没有软删除开关。

隐私:敏感字段不进日志

health_conditions 和 financial_profile 永远不出现在应用日志中。_strip_sensitive_logging 函数在结构上强制执行这一点——在任何 dict 传给 logger 之前都会调用它。这条规则是结构性的,不依赖约定。

下一步

  • 将 build_llm_context_block 接入每日简报 worker(ADR-008 worker 路径)
  • 在前端添加"我的资料"页面以读取/编辑记忆
  • Meal AI 的 worker 读取记忆获取健康状况上下文(ADR-002 已有说明)

相关 ADR: Engineering System ADR-001 · Invest AI ADR-031