写到这一篇,知了已经是"能干活的引擎",但还长着命令行的脸。本篇给它穿上浏览器界面,并讨论产品化的第一课:透明性。代码对应
zhiliao/web/server.py和web/static/index.html。
一、产品化的第一课:让用户看见 Agent 在干什么
普通 ChatBot 的界面只需要气泡;Agent 的界面必须回答另一个问题——"它现在在干嘛?"
用户问"现在几点,顺便查下 36524",界面如果沉默 5 秒后蹦出答案,用户的第一反应是"它在吗?卡了吗?靠谱吗?"。所以知了的界面把工具调用轨迹*做成 UI 的一部分:
┌─────────────────────────────────────┐
│ 你:现在几点?顺便算下 365*24 │
│ │
│ ▸ 🔧 本轮调用了 2 次工具(可展开) │
│ 1. get_current_time {} │
│ 2. calculator {"expression":…} │
│ │
│ 知了:现在是 2026-09-30 14:22… │
└─────────────────────────────────────┘
CLI 版同理(main.py 的 show_steps)。透明性不只是好看,它同时是信任建立、调试入口、以及出问题时的"事故现场"。
二、后端:FastAPI,30 行的事
架构一句话:浏览器 ⇄ HTTP API ⇄ Agent 引擎。引擎在第 3-7 篇已经造好,Web 层只是个"服务窗口":
@app.post("/api/chat")
def chat(req: ChatRequest):
sid = req.session_id or uuid.uuid4().hex[:12]
memory = get_memory(sid) # 按 session 取各自的记忆
session_registry = registry.clone() # 克隆注册表,绑定"本会话"的记忆工具
build_memory_tools(session_registry, memory)
agent = Agent(cfg, llm, memory, rag, skills, session_registry)
result = agent.run(req.message)
memory.save_session(...) # 每轮落盘,重启不丢
return {"session_id": sid, "reply": result["reply"], "steps": result["steps"]}
这里藏着一个多会话特有的坑:工具注册表是全局共享的,但 remember_fact 这类记忆工具必须绑定"当前会话"的 Memory——否则 A 用户的"记住"会写进 B 用户的记忆。解法就是上面的 registry.clone():共享工具定义,单独装配会话工具。
三个产品化的关键决策:
1. 会话隔离。 每个浏览器用户带一个 session_id,后端按 id 存取各自的 Memory——最朴素的多用户实现。前端把 id 存在 localStorage,刷新页面继续聊。
2. 引擎装配一次,请求轻处理。 llm/rag/registry 这些重对象在启动时建一次(全局单例),每个请求只创建轻量的 Agent。反例是每请求重建 RAG 索引——那会让每个请求慢 100 倍。
3. 错误结构化返回。 出错时返回 {"error": "..."} 而不是 500 白页,前端有地儿显示,用户知道发生了什么。
运行:
python web/server.py
# 浏览器打开 http://127.0.0.1:8000
三、前端:一个无框架的 index.html
全部 UI 就一个 HTML 文件(约 130 行),零前端框架、零构建工具。核心逻辑:
async function send() {
addMsg(text, "user");
const typing = addMsg("知了思考中", "typing"); // 等待提示
const resp = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: text, session_id: sessionId }),
});
const data = await resp.json();
typing.remove();
addSteps(data.steps); // ← 透明性:工具轨迹
addMsg(data.reply, "bot");
}
值得注意的产品细节:发送按钮在请求期间禁用(防重复提交);Enter 发送 / Shift+Enter 换行;消息区自动滚到底。这些小事加起来,就是"玩具"和"能用"的差别。
四、诚实的进阶清单(真实产品要补的课)
本篇的实现是"教学完整版",真实上线前还有一段路,每条都值得知道为什么:
| 缺什么 | 现状的问题 | 升级方向 |
|---|---|---|
| 流式输出 | 现在是等完整答案一起返回,长回答要干等 | SSE(Server-Sent Events)边生成边推,体感快一个数量级 |
| 鉴权 | 谁都能调 API | API Key / JWT;公网部署必须加 |
| 并发控制 | FastAPI 默认线程池扛并发,LLM 调用又慢又贵 | 队列 + 限流,防止一个用户打爆账单 |
| HTTPS | 明文 HTTP | 上线必须 TLS(云厂商一键证书) |
| 审计日志 | 对话不留痕 | 落库记录每轮的工具调用,出事能回溯 |
特别是流式输出:CLI 版的"打字机效果"其实是本地伪装的(答案已完整拿到再逐字打印);真流式要后端每拿到一小段就推给前端。第 10 篇的资源清单里有对应资料。
五、部署形态速览
知了现在的形态:单机自用(跑在你自己电脑上,数据全在本地)。
往外的三条路(第 10 篇展开):
1. 局域网共享:uvicorn --host 0.0.0.0,家人朋友同网段可访问
2. 云服务器 + Docker:一台 2 核小机即可
3. Serverless / 云托管:按量付费,适合低频使用
本篇作业
- 启动
python web/server.py,开两个无痕窗口,验证两个会话互不串台。 - 在
index.html里给工具轨迹加一个图标或颜色,体会"透明性 UI"的手感。 - 思考:如果两个用户同时发消息,共享的
registry/rag会不会出并发问题?Memory呢?(提示:一个只读一个可写——这就是为什么_sessions字典要加锁)
下一篇预告:《09 · 多 Agent 协作与进阶玩法》——一个 Agent 有极限,两个 Agent 会吵架(然后吵得更好)。写手-审稿人模式、三种协作拓扑,以及"什么时候才真的需要框架"。