Hi there 👋

Welcome to my blog. I write about programming, algorithms, and software engineering.

2026-06-26 手搓 Agent:权限护栏与 Hook 系统学习笔记

这是我手搓 coding agent 系列的第 3、4 节笔记(前两节是最小 agent loop 和多工具 dispatch)。 前两节做出来的 agent 有个吓人的特点:模型说调什么工具,循环就直接执行,包括 rm -rf /。这节就是给它装刹车,并且把刹车做成一个可扩展的系统。 一句话总结这两节的本质: s03:在工具执行前插一道检查,决定放行 / 询问 / 拒绝。 s04:把这道检查从「写死在循环里」抽象成「往事件上挂回调」,以后加功能不用再改循环。 一、s03 权限护栏:三道闸门 之前的循环是"信任模型"——handler(**args) 直接执行。s03 只做一件事:在执行之前加一段检查。整个循环、工具、dispatch 表都没动,只在执行前多调了一个 check_permission()。 核心是三道闸门,从严到松: 工具要执行了 │ 闸门1 DENY_LIST ──命中──→ ⛔ 直接拒,连问都不问 │ 没命中 闸门2 RULES ──命中──→ 闸门3 ask_user ──你按 N──→ 拒绝 │ 没命中 └──你按 y──→ 放行 │ ✅ 执行 闸门 1:硬拒绝表(deny list) 一张黑名单,命中直接拒: DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"] def check_deny_list(command: str) -> str | None: for pattern in DENY_LIST: if pattern in command: # 字符串包含就算命中 return f"Blocked: '{pattern}' is on the deny list" return None # 没命中 → None 这里有个贯穿全节的约定:命中返回理由字符串(真值),没命中返回 None(假值)。这样上层一句 if reason: 就能判断。 ...

June 26, 2026 · 3 min · 639 words · Jiaju

2026-06-26 给 Agent 加工具:Tool Dispatch 学习笔记

这篇是我学「给 Agent 加工具」(s02 Tool Use)时整理的笔记。接着上一篇手搓最小 Agent Loop往下走。 一句话总结这一节的核心: 给 agent 加一个工具,只需要"加一行"——循环代码完全不动,新工具注册进一张**派发表(dispatch map)**就行。 我用的还是通义千问 qwen-plus(OpenAI 兼容接口)。 一、为什么要给它多个工具 s01 里 agent 只有一个 bash。读文件要 cat,写文件要 echo "..." > file,找文件要 find。问题是:模型脑子里想的是"读这个文件",却要拼出一条 cat path/to/file 的命令——多了一层翻译,浪费 token,还容易拼错。 s02 的做法:给它 5 个专用工具,让它直接表达意图。 工具 干什么 read_file 读文件 write_file 写文件 edit_file 把文件里一段旧文字换成新文字 glob 按模式找文件,如 *.py bash 兜底,其它都不合适时才用 system prompt 里写一句"能用专用工具就别用 bash",模型就会优先挑合适的工具。 二、新概念:工具派发(dispatch) s01 只有 bash,模型一说要工具,代码闭眼调 run_bash 就行。现在有 5 个工具,模型说"我要 read_file",代码得按名字找到对应的那个 Python 函数去执行——这就是 dispatch。 实现靠一张派发表:一个把"工具名字符串"映射到"真正函数"的字典。 TOOL_HANDLERS = { "bash": run_bash, "read_file": run_read, "write_file": run_write, "edit_file": run_edit, "glob": run_glob, } ⚠️ 易错点:值是 run_bash,不要加括号。 ...

June 26, 2026 · 3 min · 429 words · Jiaju

2026-06-25 手搓最小 Agent Loop 学习笔记

这篇是我手搓最小 Agent Loop(s01)时整理的笔记。一句话总结它的本质: 大模型本身只会"说话"。把它的话真的执行掉、再把结果喂回去,它就从"会聊天"变成了"会干活"——这就是 agent。 我用的是通义千问 qwen-plus(走 OpenAI 兼容接口),但下面的道理换任何模型都一样。 一、Agent 到底是什么 一开始我有个困惑:大模型不是只会问答吗?它怎么还能帮我创建文件? 答案是:文件不是模型创建的,是我的代码创建的。 模型自始至终只做了一件事——输出文字。 拆开看,一个 agent 其实是三个角色凑在一起: 角色 是谁 干什么 🧠 动嘴的 大模型 只会输出文字,包括"我想用某个工具"(tool_calls) 📞 传话的 agent loop 代码 读模型的话 → 转给工具 → 把结果再转回给模型 🔧 动手的 run_bash(subprocess) 真的在电脑上执行命令 所以当我说"创建一个 hello.py",发生的是: 我说"建 hello.py" ↓ [模型] 动嘴:我想执行 echo ... > hello.py ← 只是输出文字,文件还不存在 ↓ [代码 run_bash] 动手:真的执行命令 ← ✨ 文件在这一步才诞生 ↓ [把结果喂回] → [模型] 看到成功 → 说"搞定了" ← 不再要工具,结束 关键认知:模型不会"主动调用"工具,它连发消息的能力都没有。 是我写的 for call in msg.tool_calls: 那段循环,替模型把话送到 bash 那儿。没有这段代码,模型说一万句"我想执行 ls"都没用。 ...

June 25, 2026 · 3 min · 446 words · Jiaju

2026-06-15 FastAPI 进阶:搭一个能跑 Agent 的后端

这是我学 FastAPI 进阶部分整理的笔记。和入门那篇(查询参数、请求体、response_model)不同,这次的目标很明确:搭一个能真正跑 agent 的后端——能调 LLM、能存对话记忆、能扛并发、能鉴权。 我现在是 vibe coding 的方式在学:不追求闭卷默写,而是「读得懂 AI 写的代码 + 能指出哪里不对 + 能说清自己要什么」。所以下面每个知识点我都标了两件事——它对写 agent 有什么用,以及大厂面试会怎么考。 先一句话串起全篇: 一个 agent 后端的骨架 = 接口(处理请求)+ 错误处理(诚实报错)+ 异步(扛并发)+ 数据库(存记忆)+ 依赖注入(管连接和鉴权)。 一、HTTPException:出错了要「诚实地报错」 痛点:硬返回 None 是在「撒谎」 查一只不存在的宠物,如果这么写: @app.get("/pets/{pet_id}") def get_pet(pet_id: int): pet = pets_db.get(pet_id) # 没找到,pet = None return pet # 直接返回 None 后果是:状态码还是 200 OK,body 是 null。这等于明明失败了却告诉前端「成功了」。前端通常这么判断: if (response.ok) { // 200 → ok 是 true,以为成功了 showPet(data.name); // data 是 null → 💥 报错 "Cannot read 'name' of null" } 更糟的是用中括号取值 pets_db[999]——直接 KeyError,FastAPI 兜底返回 500,把「用户查了个不存在的 id」(用户的锅,4xx)甩锅成「服务器坏了」(服务器的锅,5xx)。 ...

June 15, 2026 · 5 min · 952 words · Jiaju

2026-06-11 FastAPI 入门:查询参数、请求体与接口调试

这是我学 FastAPI 时整理的入门笔记。FastAPI 是现在最流行的 Python 后端框架之一,我用它完整走了一遍「写接口 → 启动服务 → 测试 → 读懂报错」的闭环。 一句话总结它的本质: 用 Python 写「后端接口(API)」的框架,靠类型注解自动帮你校验数据、生成文档。 下面按我实际学习的顺序展开。 一、FastAPI 是什么 先拆解几个词: 后端接口(API):你刷新页面、点开 App 时,背后有个服务器在「收到请求 → 处理 → 返回数据」。写这个逻辑就是写后端,对外的"窗口"就是 API。 框架(framework):别人提前搭好的脚手架,你只填业务逻辑,不用从零造轮子。 类比:你开餐厅,FastAPI 就是已经装修好的厨房——灶台水管都通了,你只管炒菜。 它这几年特别火,三个杀手锏: 快:性能在 Python 框架里数一数二(基于异步 asyncio)。 自动生成交互式文档:写完代码自动给你一个能点击测试的网页。 自动校验数据:声明字段类型,传错自动报错,不用手写校验。 对新手尤其友好:它强依赖 Python 类型提示,学它的同时 Python 基本功也会变扎实。 二、最小可运行例子 from fastapi import FastAPI app = FastAPI() # 创建一个"应用"实例 @app.get("/") # 当有人 GET 访问根路径 "/" 时... def read_root(): return {"hello": "world"} # ...就返回这个 JSON 逐行看: 代码 人话解释 app = FastAPI() 开张,建一个 app @app.get("/") 装饰器:下面这个函数负责处理 GET 访问 / 的请求 def read_root() 真正干活的函数 return {...} 返回的字典会被自动转成 JSON 发出去 三、接收数据的三件套 写接口的核心就是「怎么接收别人传来的数据」。FastAPI 有三种方式。 ...

June 11, 2026 · 4 min · 653 words · Jiaju

2026-06-11 FastAPI 响应模型与请求体学习笔记

这篇是我接着上次学 FastAPI 时整理的笔记。上次学的都是「数据怎么进来」(查询参数、请求体、校验),这次换了个方向,研究「数据怎么出去」——也就是 response_model,顺带把困扰我很久的「请求体 vs 查询参数」彻底分清楚了。 一句话总结这次的核心: response_model 是给接口出口装的筛子:你规定它只准返回哪些字段,多余的(比如密码)自动被丢掉。 下面按「响应模型 → 请求体 vs 查询参数 → 路由和参数到底是什么 → 最该记住的几点」来展开。 一、响应模型 response_model 1. 先讲痛点:返回数据会泄露敏感字段 在 FastAPI 里有条铁律:你 return 什么,前端就收到什么。 假设数据库里的用户带密码字段: class User(BaseModel): username: str password: str # 敏感字段! email: str @app.post("/users/") async def create_user(user: User): return user # 灾难:把 password 原样返回给了前端 这样写,密码就跟着 JSON 一起发出去了。response_model 就是用来堵这个漏洞的。 类比一下:Pydantic 请求体像安检进站(检查你带进来的东西);response_model 像安检出站(检查你带出去的东西,违禁品没收)。 2. 怎么用:进、出各写一个模型 经典套路是定义两个模型——一个收(带敏感字段),一个发(不带)。 from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() # 进:注册时前端要传的(含密码) class UserIn(BaseModel): username: str password: str email: str # 出:返回给前端的(不含密码!) class UserOut(BaseModel): username: str email: str @app.post("/users/", response_model=UserOut) # 关键在这 async def create_user(user: UserIn): return user # 即使返回了带 password 的对象,也会被过滤成 UserOut 神奇之处:函数里 return 的明明是带密码的 user,但前端收到的 JSON 里根本没有 password。FastAPI 在出门那一刻,按 UserOut 重新「拓印」了一份。 ...

June 11, 2026 · 3 min · 471 words · Jiaju

A Scroll-Animated Roadmap Timeline for My Projects

While rebuilding the landing page of TicketCoach — my AI sparring-partner system for customer-support training — I replaced the usual feature grid with a progress timeline: one vertical line, one dot per milestone. Shipped work glows in a purple gradient; future work stays gray and dashed. As you scroll, the bright line literally grows down the page and stops exactly at the last completed dot. I ended up liking this little UI more than the product page itself. So I extracted it into a Hugo shortcode, and from now on every project I write about here gets one of these. This post is both the first real use of it and a short note on how it works. ...

June 10, 2026 · 4 min · 732 words · Jiaju

RAG 入门:在模型回答之前,先检索资料

这篇是我学 RAG 时的白板笔记整理。RAG(Retrieval-Augmented Generation,检索增强生成)是现在几乎所有"让 AI 读自己资料"产品的底层套路,比如企业知识库问答、文档助手、客服机器人。 一句话总结它的本质: 在模型回答之前,先检索资料,再基于资料生成回答。 下面按"为什么需要它 → 它怎么工作"的顺序展开。 一、为什么需要 RAG 1. 直接把所有资料喂给模型,为什么不可行? 最朴素的想法是:把全部文档塞进 prompt,让模型自己看。这条路有三个问题: 上下文窗口有限。模型一次能读的 token 数是有上限的,几百页的文档根本塞不下。 成本。就算塞得下,每次提问都把全部资料发一遍,token 费用会爆炸。 响应速度。输入越长,模型处理越慢,用户体验直线下降。 2. 模型本身也有短板 就算不考虑塞资料的问题,纯靠模型自己回答也有硬伤: 知识过时。模型的知识停留在训练截止日期,问它昨天发生的事它不知道。 私有数据。公司内部文档、个人笔记,模型训练时根本没见过。 幻觉。不知道的事情它可能一本正经地编。 RAG 的思路就是绕开这些限制:资料放在模型外面,每次只把和问题相关的一小段喂进去。 二、核心流程 整个 RAG 分两个阶段:数据准备(离线,只做一次)和用户提问(在线,每次提问都执行)。 flowchart LR subgraph prep["① 数据准备(离线,只做一次)"] direction LR A["📄 原始文档"] --> B["切分 Chunking按章节 / 段落 / 字数……"] B --> C["🧩 Chunks每一块语义相对完整"] C --> D["Embedding(向量化)"] D --> E[("向量数据库")] end flowchart LR subgraph query["② 用户提问(在线,每次提问都执行)"] direction LR Q["❓ 用户问题"] --> QE["问题也做Embedding"] QE --> S["相似度检索找距离最近的 top-k 块"] V[("向量数据库")] --> S S --> P["相关资料 + 问题拼成 Prompt"] P --> L["🤖 LLM 生成回答"] end 第一步:数据准备 切分(Chunking) 文档不能整篇存,要先切成一块块的 chunk。切分的依据可以是章节、段落、字数……没有标准答案,但有一条核心原则: ...

June 10, 2026 · 1 min · 207 words · Jiaju

A Records vs CNAME Records: How to Point a Domain at Your Website

I recently bought a domain and pointed it at my GitHub Pages site. It sounds like a one-click thing, but it comes down to two little DNS records: an A record and a CNAME record. Once I understood what each one does, the whole process stopped feeling like magic. Here’s the explanation I wish I’d had. A 30-Second DNS Refresher When someone types example.com into a browser, computers don’t actually understand names — they only talk to IP addresses (like 185.199.108.153). DNS (Domain Name System) is the giant phone book that translates a human-friendly name into a machine-friendly address. ...

June 9, 2026 · 4 min · 825 words · Jiaju

博客开发日志(中文):改动记录与前端原理

这是一篇按日期累积的开发日志。每次改动博客,我都把"做了什么"和"学到的前端原理"记在这里,方便以后翻查。最新的改动放在最上面。 维护约定:下次改动时,在 改动模板 上面新增一个 ## 日期 — 标题 小节即可。 2026-06-09 — 接入自定义域名 + 翻译按钮升级 这次做了什么 自定义域名上线:把 GoDaddy 买的 xungirl.com 接到 GitHub Pages,配置了 DNS(4 条 A 记录指向 GitHub IP + 1 条 www 的 CNAME),清掉了 GoDaddy 默认的停放记录,最后开启了自动 HTTPS(Let’s Encrypt 证书)。 新增文章:一篇讲 A 记录 vs CNAME 记录的英文文章。 翻译按钮:保护代码与公式:点翻译时,代码块、行内代码、数学公式保持原样不被翻译。 翻译按钮:升级为双向:自动判断当前文章是中文还是英文,翻成"另一种"——英文文章翻中文、中文文章翻英文。 涉及文件:layouts/partials/extend_head.html(翻译逻辑)、content/posts/(文章)。 前端原理 这次最值得记的是几个前端基础概念,比具体代码更重要。 1. 构建期 vs 运行期(最核心的一条线) 构建期(Hugo) 运行期(浏览器 JS) 谁在做 push 后 GitHub Actions 构建一次,生成静态 HTML 访客打开页面时,浏览器实时执行 适合 文章内容、菜单、固定文案(可走 Hugo 的 {{ i18n }} 翻译) 会动的东西:计时器、实时翻译、交互 特点 生成完就定死,之后不变 每次访问都重新跑,可以随时变 为什么重要:博客底部那个"已运行 X 天 X 时"的计时器,必须用运行期 JS(因为每秒都在变,Hugo 构建一次没法让它跳动);而它显示的英文字是写死在 JS 里的,脱离了 Hugo 的翻译体系,所以切语言不会变——这不是 bug,是它本来就在另一套系统里。 ...

June 9, 2026 · 2 min · 304 words · Jiaju