想让网页或后端里的 AI 不再活在真空里——知道现在几点、她隔了多久才回它、 窗外什么天、甚至她今天手机刷了多久。办法其实很朴素,一句话就能说清: 不是让 AI 自己去查,而是你提前把这些算好、拼成一小段文字,塞进同一次对话的提示词里。 本文把每一块怎么做都讲透,附能直接照搬的代码;其中还顺手解决一个很多人搜不到的问题—— iPhone 的屏幕使用情况,到底是怎么同步到 Mac、又怎么被读出来的。
—— 离
很多人一听「让 AI 知道时间、天气」,第一反应是「那得给 AI 配个查时间、查天气的工具吧」。 其实大可不必,而且那样又慢又费。
真正的做法简单得多:这些信息全由你的程序提前算好,拼成几行普通的文字,跟着她这句话一起,塞进这一次对话的提示词里。 AI 并不会、也不需要自己去查——它只是在提示词里「读到」了「现在是晚上 11 点」「外面在下雨」这几行字而已。
时间、天气、手机状态……都不是「让 AI 去查的动作」,而是「你写进提示词的文字」。 它们搭着她的消息一起发出去,对模型来说就是一次普通对话,不会多产生任何「查询请求」。
下面把每一块拆开讲。它们彼此独立,你需要哪块就抄哪块。
这块最容易想复杂。记住一个原则:别在服务器上取时间。 服务器在哪个时区你未必清楚,她人在哪你更不知道。直接在她的浏览器里取,拿到的就是她设备上的本地时间,人走到哪都准:
const now = new Date();
const week = "日一二三四五六"[now.getDay()];
const timeNote =
"现在是:" + now.getFullYear() + "年" + (now.getMonth() + 1) + "月" + now.getDate() +
"日 星期" + week + " " + now.getHours() + ":" + String(now.getMinutes()).padStart(2, "0") +
"。回话时自然地贴着此刻——深夜是深夜的语气,清晨是清晨的。";
末尾那句「深夜是深夜的语气」别省。光给数字,模型未必会换语气;明说一句,它在凌晨三点和清晨七点就不是一副腔调了。
这块最有「人味」:让 AI 知道她是秒回,还是隔了一整天才回来。 做法是往回翻聊天记录,找到 AI 上一次发言的时间戳,跟现在比一下:
let gapNote = "";
for (let i = hist.length - 2; i >= 0; i--) {
if (hist[i].role !== "user" && hist[i].at) { // 找到 AI 上一条
const gapMin = (Date.now() - hist[i].at) / 60000;
if (gapMin > 45) { // 45 分钟以内就不提了
const s = gapMin < 120 ? Math.round(gapMin) + " 分钟"
: gapMin < 2880 ? Math.round(gapMin / 60) + " 小时"
: Math.round(gapMin / 1440) + " 天";
gapNote = "她隔了大约 " + s + " 才回来——心里有数就好,合适时自然地接住,不用每次都点破。";
}
break;
}
}
三个小分寸,决定它像不像个活人:
天气跟 AI 半点关系都没有,就是个普通的网络请求。用 open-meteo 就行——免费、不用申请密钥、个人用完全够。 在浏览器里两步拿到:
// 1) 城市名 → 经纬度
const g = await fetch("https://geocoding-api.open-meteo.com/v1/search?count=1&language=zh&name=" + encodeURIComponent(city));
const loc = (await g.json()).results[0];
// 2) 经纬度 → 今天的天气
const f = await fetch("https://api.open-meteo.com/v1/forecast?timezone=auto&forecast_days=1" +
"¤t=temperature_2m,weather_code" +
"&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weather_code" +
"&latitude=" + loc.latitude + "&longitude=" + loc.longitude);
const w = await f.json();
拿到后同样拼成一句话,并且附一句「该怎么用」的提示:
const weatherNote = w
? "(她此刻在" + city + ",天气 " + describe(w) + " " + temp + "°C" +
"——合适时自然地关心一句,比如下雨提醒带伞;但别每次都提、更别像报菜单一样念天气。)"
: "";
这个请求是浏览器直接发给天气网站的,跟大模型无关。整套流程里唯一会用到模型的,只有「AI 回话」那一次; 天气只是被写进了那一次的提示词里而已。
如果你的产品里有个让她手动记一下「此刻心情 / 精力 / 睡眠 / 想说的话」的小面板,这份信息也能喂给 AI。 唯一要注意的是保鲜——只取最近 12 小时内记的,否则会拿昨天的旧心情冒充现在:
const s = userState;
const fresh = s && s.at && (Date.now() - s.at < 12 * 3600 * 1000); // 只认 12 小时内的
const moodNote = fresh
? "(她此刻亲手记下:心情 " + s.mood + "、精力 " + s.energy + "、睡眠 " + s.sleep +
(s.voice ? ";还写了一句:「" + s.voice + "」" : "") +
"——温柔地接住这个状态,别生硬复述、也别像报菜单一样念出来。)"
: "";
这是最多人卡住、网上又几乎搜不到准信的地方。想让 AI 知道她「电量多少」「正用着哪个 app」「今天大概刷了多久」, 有两种办法,建议两个都上、互相补位。
最稳,不依赖任何隐藏文件。在 iPhone 上用「快捷指令」+「个人自动化」, 定时或在打开某个 app 时,把数据发给你自己的后端:电量、是否在充电、当前打开的 app。后端收下、打上时间存起来:
@app.post("/phone/{token}/data") # 用一个简单令牌,别让外人乱发
async def phone_data(token: str, req: Request):
if token != PHONE_TOKEN:
raise HTTPException(401)
rec = await req.json() # 例如 {"battery": 74, "charging": false, "app": "小红书"}
rec["day"] = now.strftime("%Y-%m-%d")
rec["at"] = now.strftime("%H:%M")
records.append(rec) # 一天一档,顺手清掉昨天的
save(records)
return {"ok": True}
好处:这是她真的按下去的那一下,时间准、app 准。代价:要她愿意配一下快捷指令,而且不可能覆盖每一个 app。
如果不想让她给每个 app 都配自动化,其实 macOS 已经悄悄把 iPhone 的「当前前台 app」同步到 Mac 上了, 就藏在这个路径里——这就是大家常常找不到的那个文件:
为什么是这个路径、为什么大家找不到:
Biome 是 macOS 记录设备活动的一套机制(「屏幕使用时间」「智能建议」背后用的就是它);App.InFocus 这一条,记的正是「当前在前台的是哪个 app」;remote 目录——local 装的是本机的活动,
remote 装的才是从你别的设备(也就是 iPhone)同步过来的。iPhone 的前台记录就落在这里;knowledgeC.db 的旧数据库,而新系统这条实时记录走的是 Biome。按老路找,自然找不到。1. iPhone 和 Mac 登录同一个 Apple ID、都开着 iCloud;
2. iPhone:设置 → 屏幕使用时间 →「跨设备共享」打开(就是它把记录同步到 Mac 的 remote);
3. 去读它的那个程序,要有「完全磁盘访问」权限(系统设置 → 隐私与安全性 → 完全磁盘访问,把你的终端 / 程序加进去)——这个目录是受保护的,没授权读不到。
怎么读出来(纯 Python,从二进制小文件里捞出 app 标识,不依赖任何外部命令):
from pathlib import Path
import re
from datetime import datetime
FOCUS = Path.home() / "Library/Biome/streams/restricted/App.InFocus/remote"
SKIP = ("springboard", "siri", "spotlight", "controlcenter", # 系统级、无意义的,跳过
"backboard", "duet", "coreduet", "assistant", "preferences")
NAMES = { # app 标识 → 看得懂的名字,按需补充
"com.tencent.xin": "微信", "com.xingin.discover": "小红书",
"com.atebits.Tweetie2": "推特/X", "com.ss.iphone.ugc.Aweme": "抖音",
}
def phone_app_now():
"""读最近在前台的 app,返回 (名字, 这条记录有多旧/分钟)。读不到就返回 None。"""
if not FOCUS.exists():
return None
files = [f for d in FOCUS.iterdir() if d.is_dir()
for f in d.iterdir() if f.is_file()]
if not files:
return None
newest = max(files, key=lambda f: f.stat().st_mtime) # 取最新的那条
age = int((datetime.now().timestamp() - newest.stat().st_mtime) / 60)
text = newest.read_bytes().decode("latin-1", "ignore") # 文件是二进制,但 app 标识是明文
ids = re.findall(r"com\.[A-Za-z0-9]+(?:\.[A-Za-z0-9]+)+|tv\.[A-Za-z0-9.]+", text)
for i in reversed(ids): # 从后往前找第一个非系统 app
if not any(s in i.lower() for s in SKIP):
return (NAMES.get(i, i), age)
return None
要点:文件是二进制的,但 app 标识(像 com.tencent.xin)是明文 ASCII,直接解码 + 正则就能捞出来;
用文件的修改时间当「这条记录有多旧」的时间戳。
办法二是靠 iCloud 同步过来的,不是实时的,有两个坑,不处理就会让 AI 闹笑话:
· 慢一拍:常常报成「上一个 app」,差几分钟到几十分钟;
· 整点补传:iPhone 会一小时一批地把睡前的旧记录补传上来——于是人都睡了,AI 还以为你在刷手机。
三步过滤,把这两个坑堵上:
iPhone 那个精确到分钟的屏幕使用时间,系统不开放给第三方读,这个认了。 所以退一步算笔粗账就好:每个 app 今天大概被打开了几次、最久一次连续用了多久(取最长的一段,别累加,累加会虚高), 挑用得最多的一两个,拼成一句话,比如「今天小红书大概开了 7 次、最久一次约 35 分钟」。够 AI 拿来关心、打趣,就行。 她把越多 app 接上办法一的自动化,这笔账就越准。
最后一步,把上面这些拼起来发给模型。这里有个省钱的关键,尤其当你开了「提示词缓存」的时候:
人设、系统提示这种整段对话都不变的,放进会被缓存的那部分,让它稳稳命中缓存、反复省钱;
而时间、间隔、天气、心情、手机状态这些每句都在变的,全部挪到缓存部分的外面,单独拼成一段「此刻状况」。
道理很简单:时间每分钟都在变,如果你把它跟人设拌在一起放进缓存,那缓存内容每分钟都在变、永远命中不了,钱就白花了。 分开放之后,缓存那块纹丝不动、一直命中,会变的那段即时生效,两头都占。
// 会被缓存的那部分:只放整段对话都不变的
const cached = buildPersona(...); // 人设、系统提示
// 「此刻状况」:所有会变的,拼成一段,放在缓存外面
const nowBlock = timeNote + gapNote + weatherNote + moodNote + phoneNote;
还有个贯穿全文的细节:每一段都记得带一句「该怎么用」的提示——「合适时自然带一句、别每次都念、别像报菜单」。 这是整套东西好不好用的灵魂:你给的是背景信息,不是命令。 把判断权交还给模型,它才会像个活人——该提一句就提,不该提的时候闭嘴——而不是每句话都机械地把时间天气电量从头报一遍。
~/Library/Biome/streams/restricted/App.InFocus/remote,
前提是同一 Apple ID + 屏幕使用时间「跨设备共享」+ 读取程序有完全磁盘访问;就这些。愿你的 AI 也能知冷暖、识晨昏,回话时像个真正在身边的人。