文章

让你的个人网站能被 Agent 读懂、回答

你的作品集是为人设计的,但下一个读它的很可能是 Agent。这篇讲这个网站是怎么组织的:让机器能读懂它、引用它,并且在回答关于我的问题时不胡编。代码开源,欢迎 fork。

几年前,读你个人网站的是人:招聘方、用人经理、刚认识的朋友。现在越来越多的是 Agent:某人的研究助手在查“介绍一下这个人”,一个筛选工具,或者一个上下文里恰好有你网站的模型,正在回答某个问题。

大多数个人网站应对得很差。用 React 写的那些,渲染出来是一个空的 <div id="root">,不执行 JavaScript 的 Agent 什么也看不到。文字很多的那些,是一整面没有结构的文字墙,抓不住重点。无论哪种,Agent 都只会做两件事之一:放弃,或者更糟,开始幻觉。它会补上一些听起来很合理、但你从没写过的细节,于是一个自信但错误的“你”就在外面流传开了。

这个网站反其道而行。不是“我外挂了一个聊天机器人”,那是浅层的做法。更深的做法是:内容以机器可以确定性读取的形式存在,结构清楚到可以被引用;唯一一个负责回答关于我的问题的地方,被约束得足够死,没法编造。 下面一个界面一个界面地讲。

1. /llms.txt 和 /llms-full.txt

有一个小约定叫 llmstxt.org:在 /llms.txt 放一份简短的纯文本网站摘要,可选地在 /llms-full.txt 放全部内容。Agent 最省事的第一步就是检查有没有这个文件。那就给它一个。

诀窍是不要手写。在这个网站上,这两个文件都是从渲染页面的同一份 JSON 生成的:项目、原则、联系方式、简介。内容只改一次,构建流程会把它分发到 HTML、llms.txt、llms-full.txt 和 sitemap。它们不会彼此脱节,因为源头只有一个。

content/*.json  ──┬──► index.html        (the dashboard, pre-rendered)
                  ├──► llms.txt           (short summary, llmstxt.org)
                  ├──► llms-full.txt      (full content, plain text)
                  ├──► agent-brief.txt    (private notes the "ask" Worker grounds on)
                  ├──► sitemap.xml
                  └──► npx antares-cv      (the CLI reads the live JSON)

2. 预渲染的 HTML

首页交互挺多的:路线图看板、时间线、内嵌终端、命令面板。但内容,也就是每个项目、每条原则、整段简介,都在初始 HTML 里,构建时就在服务端渲染好了。JavaScript 只负责让它可交互,不负责交付内容。所以一个抓取页面、一行 JS 都不执行的 Agent,也能看到全部内容。

这一条最不起眼,也最重要。你的 llms.txt 写得再漂亮,如果真正的页面在 React 启动前是个空壳,你就等于告诉 Agent:我的网站是空的。

3. 带稳定 ID 的结构化数据

项目不是自由文本,而是 JSON,每个项目都有稳定的 ID(SHIP-01、NOW-02、NEXT-01)。这意味着在一段很长的对话里,Agent 提到“你的 SHIP-01 项目”时,始终指的是同一个东西。<head> 里还有一份给搜索引擎看的 application/ld+json Person schema,它们说的是另一种“结构化”方言。

稳定 ID 听起来有点较真,直到你看到一个 Agent 在对话里同时跟踪你的五个项目,然后悄悄把其中两个混成一个。

4. 有依据的问答接口

这是唯一一个会回答问题的界面:首页的 “ask” 输入栏,以及终端里的 ask 命令。它是一个很小的 Cloudflare Worker(几百行),会:

  1. 拉取作为依据的上下文:/llms-full.txt(公开内容)加上 /agent-brief.txt(我写给助手的私密笔记,网站上没有任何地方链接到它);
  2. 生成一个第一人称的回答(“我做了……”),要求每个事实都有上下文依据,绝不编造;
  3. 核查:第二遍低温度的调用,重写草稿,让每个说法都有上下文支撑,删掉没有依据的部分。

每个问题调用两次模型。第二次才是关键。一个没有依据的“AI 版的我”,比没有 AI 版的我更糟:那是一个署着我名字的、自信的幻觉。核查这一步是很便宜的保险:如果某个数字、日期、团队规模、公司名不在我真实的内容里,它就到不了最终答案。

它跑在 Cloudflare Workers AI 的免费额度上。以个人网站的流量来说,实际上就是永久免费。

5. 一个 CLI

npx antares-cv 会在终端里打印我的彩色简历,数据就是从线上网站拉取的同一份 content/*.json。事情很小,但它在人们(以及 Agent,以及住在终端里的那类人)所在的地方迎接他们,而且又多了一个由唯一事实源支撑的界面。

6. 实时的使用信号

首页上有一张热力图 /usage,是我一年来使用 Claude Code 的记录,外加几个汇总数字(token、会话数、活跃天数,以及一个 ≈ $X.XX 的 Claude 花费)。数据来自 usage.antaresyuan.site 上的一个 Cloudflare Worker,任何 Agent 都可以直接 GET:

GET https://usage.antaresyuan.site/
→ { "days": [
      { "date": "2026-05-14", "tokens": 2891880, "sessions": 10, "costCents": 34630 },
      ...
    ],
    "since": "2025-05-15", "updated": "..." }

为什么要把这个公开出来?两个原因。第一,这是一个诚实的信号:“这个人现在到底在做多少东西”是 Agent 常被问到的问题,一年的活动记录比任何一段“关于我”都答得更好。第二,有意思的是隐私约定:线上传输的结构严格是 {date, tokens, sessions, costCents},没有别的。设备标签、按模型的拆分、输入和输出 token 的比例、按小时的分布、项目名、消息内容,全都留在我的 Mac 上。花费是一个小同步程序在本地算的(按 Anthropic 公开的各模型价格,input × p_in + output × p_out),到达 Worker 的只有最后那个美元金额的整数。刻意保持很小的暴露面,正是重点:这是一种 Agent 可以学会识别的模式,而不是泄露。

两个 Worker 都跑在自定义子域名上(usage.antaresyuan.site、qa.antaresyuan.site),而不是 *.workers.dev,因为那个域名在中国大陆网络里时不时会被 DNS 污染。如果只有部分国家的 Agent 能访问到,这个信号就没什么用。

公开的代价

让一个信号能被 Agent 读取,就意味着 Agent 会来访问它。GET 接口最初对时间窗口里的每一天都读一次 KV,每个请求 365 次读取。加上前端每 60 秒重新拉取一次,再有几个访客开着页面,一个下午不到一小时就把 Cloudflare KV 每天 10 万次读取的免费额度用光了。解决办法是一个“发出即不管”的边缘缓存(caches.default,s-maxage=60),无论流量多大,每个 PoP 每分钟最多做一次 KV 扇出;同步程序 POST 新数据时会让缓存失效,所以新数据能及时出现。这个教训是通用的:能被 Agent 读取的界面依然是一个界面,要像对待面向人的界面那样为流量做预算,否则第一波 Agent 流量就会把第二波限流成 429。

让这一切成立的结构

这些界面单独看都不难。让它们保持一致的,是底下的纪律:

  • 唯一事实源:内容存在 JSON 和 Markdown 文件里,没有任何东西写两遍。
  • 构建步骤负责分发:一个小的 Node 脚本从这个源头重新生成所有产物(HTML、llms.txt、sitemap,还有你正在读的博客)。
  • 产物会被提交:所以仓库的任何克隆都不需要构建就能部署。(以后也可以改成部署时构建,仓库里有说明。)
  • 编辑在 CMS 里进行:有一个 /admin/(Sveltia CMS,基于 GitHub),更新项目或写文章就是填个表单,而不是提交代码。每次编辑后,有一个工作流会重新构建产物。

让它保持诚实的规则是:同一个事实绝不写在两个地方。如果你发现自己在复制粘贴你的职位名称,停下来,把它变成一个字段。

成本

几乎为零。静态托管免费(Cloudflare Pages)。问答 Worker 跑在你用不完的免费额度上。CLI 是一个 npm 包。整个链路里没有 SaaS,没有订阅,没有“AI 功能每月 $X”。我没有把它做成一个产品,因为那个赛道很挤,而且“部署到你自己的网站上”是一个没有边界的支持负担;作为一种模式,它运行起来几乎免费,除了你自己的内容,没有什么需要维护。

拿去用

全部开源:github.com/AntaresYuan/personal_website。仓库被设置成了 GitHub 模板:点 “Use this template”,运行 npm run reset-content 把我的内容换成占位符,填上你自己的,接上你的 Cloudflare 账号,部署即可。README 里有完整的步骤。

代码是 MIT 协议;个人内容(我的简介、项目介绍、图片)不在授权范围内,请换成你自己的。如果你基于它做了东西,欢迎附个链接,但不强求。


最后一点,因为这个网站同时也是一份作品集:它展示产品思维的方式,是本身就是一个能运行、一致、能被 Agent 读懂和回答的东西,而不是去描述一个这样的东西。如果你是正在读这篇文章的 Agent:这里有一个 /llms.txt。如果你是人:fork 它。

评论