- 人工智能
- AI 应用
- AI 评测
- RAG
- 微调
- 提示工程
- 开发工具
- 桌面应用
【免费下载链接】Kiln
Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.
本文围绕 Kiln 仓库中specs/projects/chat_app_state/functional_spec.md这份功能规格展开:介绍 Kiln 如何向聊天 Agent 注入一段不可见的<new_app_ui_context>应用状态头(app context header),使其了解用户当前所处的页面、正在查看的 project 与 task。读完本文,你可以掌握“页面元信息 store + 增量状态比对 + 发送时前缀注入 + CI 覆盖率测试”这套完整的上下文注入方案,并能在 agent.ts、chat_session_store.ts 等源码中找到每一处实现落点。
1. 机制总览:只在状态变化时注入上下文
当用户发送一条聊天消息时,Kiln 会检查自上一条消息以来应用状态(app state)是否发生变化:只有发生变化时,才在消息文本前拼接一段不可见的<new_app_ui_context>块。这样做的目的是让聊天 Agent 知道“用户此刻在应用的哪个位置、正在看什么”,同时避免在每一条消息里重复发送未变化的上下文,节省 token 并降低噪音。
整个机制由三部分组成(对应规格中的 Phases 设计):
agentInfostore:每个 Svelte 页面声明自己的名称与描述;lastSentAppState追踪 + 上下文头拼接逻辑:在聊天会话 store 的发送流程中完成状态比对与注入;- CI 覆盖率测试:静态扫描所有
+page.svelte,确保新页面不会遗漏agentInfo声明。
2. App State 数据模型
应用状态由两个来源的字段组成,完整定义见 functional_spec.md:
| 字段 | 来源 | 示例 |
|---|---|---|
Path | 浏览器 URL 路径(window.location.pathname) | /tools/189194447825/add_tools |
Page Name | agentInfostore | Settings Home |
Page Description | agentInfostore | A page for managing user settings... |
Current Project | UIState store | 12345 |
Current Task | UIState store | 123456 |
在源码中,这一模型对应 agent.ts 的AppState接口:
export interface AppState { path: string pageName: string | null pageDescription: string | null currentProject: string | null currentTask: string | null }getCurrentAppState()(agent.ts#L35-L48)负责采集这五个字段:读取agentInfostore、ui_statestore(提供current_project_id/current_task_id)以及window.location.pathname。其中路径读取做了 SSR 保护:typeof window !== "undefined" && window.location不成立时返回空字符串。从源码结构看,这段逻辑实际只在客户端发送消息时执行,SSR 分支主要是防御性代码。
3.agentInfoStore:页面自我描述
agentInfo是一个 Svelte writable store,定义在 agent.ts:
export interface AgentPageInfo { name: string description: string } export const agentInfo = writable<AgentPageInfo | null>(null)规范要求每个+page.svelte都必须调用agentInfo.set(...)提供 name 与 description。描述文案分两种策略:
- 简单页面用静态文案,例如:
"A page for managing user settings. General selection screen with many options." - 需要时动态生成,例如:
"Tool detail page for tool ID 100945512896 in project 1234545, named 'Memory by Anthropic'"
描述应包含关键上下文数据(选中实体的名称、激活的过滤器、带标签的 ID 如 "project ID X"),但不必穷举页面全部数据——只给 Agent“最需要知道的”。
仓库中已有大量页面按此模式声明。两个典型示例:
静态路由 + 动态拼接(add_tools 页面/tools/[project_id]/add_tools/+page.svelte#L14-L17)):
$: project_id = $page.params.project_id! $: agentInfo.set({ name: "Add Tools", description: `Add tools to project ID ${project_id}. Choose from local MCP, remote MCP, Kiln task tools, and pre-made tool servers.`, })依赖异步数据、带加载态兜底(tool server 详情页/tools/[project_id]/tool_servers/[tool_server_id]/+page.svelte#L25-L28)):
$: agentInfo.set({ name: "Tool Server Detail", description: `Tool server detail for server ID ${tool_server_id} in project ID ${project_id}. Server name: ${tool_server?.name ?? "[loading]"}. Shows server properties, tools, status, and configuration.`, })注意这里使用 Svelte 响应式语句$::路由参数或加载的实体数据变化时,描述会自动重新设置,从而始终反映“用户当前正在看的实体”。
4. 上下文头格式与增量规则
完整格式
上下文头采用类 XML 块,字段行按固定顺序输出:
<new_app_ui_context> Path: /tools/189194447825/add_tools Page Name: Settings Home Page Description: A page for managing user settings. Current Project: 12345 Current Task: 123456 </new_app_ui_context>字段标签映射定义在 agent.ts#L19-L25 的FIELD_LABELS(path→Path、pageName→Page Name、pageDescription→Page Description、currentProject→Current Project、currentTask→Current Task),输出顺序由FIELD_KEYS数组固定,保证 Agent 看到的内容结构稳定。
只包含变化字段
上下文头只包含相对上次发送状态发生变化的字段。例如只有路径和页面变了、project/task 未变时:
<new_app_ui_context> Path: /tools/189194447825/add_tools Page Name: Tool Detail Page Description: Tool detail page for tool 100945512896, named 'Memory by Anthropic' </new_app_ui_context>实现见buildContextHeader()(agent.ts#L50-L69):逐字段比对current与lastSent,将不同的字段收集到changed字典;若lastSent为null(首条消息)则输出全部非空字段;若无任何变化则返回null,发送流程不加头。
一个规格未明说、但源码明确处理的细节:当某个字段从有值变为 null/空时,变更头会以(none)占位输出(见 formatChangedHeader),以显式告知 Agent“该上下文已消失”;而首次发送的完整头(formatHeader)则直接跳过 null 与空字符串字段,全部为空时返回空串。
Null 处理
若agentInfo为null(页面尚未设置),则省略Page Name与Page Description两个字段——至少会发送Path。规格中说明由第 7 节的 CI 测试保证“页面忘记设置”的情况在实践中不会发生。
5. 发送逻辑:在beginStreaming中注入
规格将发送逻辑概括为五步:
- 采集当前 app state:读
agentInfo、UIState(project/task)与window.location.pathname; - 从聊天会话 store 读取
lastSentAppState(新会话为null); - 逐字段与
lastSentAppState比对; - 若有字段变化(或
lastSentAppState为null——首条消息):用仅含变化字段的块(首条消息则含全部字段)构建<new_app_ui_context>,前缀拼接到发给后端 API 的消息文本,并用完整当前状态更新lastSentAppState; - 若无任何变化:原样发送消息,不加头。
实际落点在 chat_session_store.ts 的beginStreaming中(sendMessage的入口):
// updateMessages 存入的是“干净”的 userMessage(不含 header) updateMessages((msgs) => [...msgs, userMessage, assistantMessage]) const currentAppState = getCurrentAppState() const header = buildContextHeader( currentAppState, get(persisted).lastSentAppState, ) let apiMessage = userMessage if (header) { apiMessage = { ...userMessage, content: header + "\n" + text } } persisted.update((p) => ({ ...p, lastSentAppState: currentAppState, }))随后streamChat收到的messages参数是带头的apiMessage,而持久化的messages数组里保存的是不含头的userMessage。
UI 不可见性的保证
上下文头在聊天界面中不可见:它只加在发往 API 的 payload 上,界面上显示的用户消息只有用户输入原文。这意味着:
- 持久化会话中的
ChatMessage保留原始用户文本(无 header); - header 仅在发送时刻(
streamChatpayload 中)添加。
另外,chat_session_store.ts#L186-L191 的埋点事件chat_message_sent带有has_app_context_header: !!header字段,从产品侧也能验证“是否随消息发送了上下文头”。
6.lastSentAppState的持久化与重置
lastSentAppState作为持久化字段挂在PersistedChatSession上(与 messages 一起存储在 session storage,键为kiln_chat_session):
export interface PersistedChatSession { messages: ChatMessage[] collapsedPartKeys: Record<string, boolean> lastSentAppState: AppState | null // new }见 chat_session_store.ts#L23-L27。相关行为均有明确出处:
- 新会话 /
reset():EMPTY_PERSISTED与reset()都会把lastSentAppState置回null(L68-L72、L357-L374),保证任何会话的第一条消息总是携带完整上下文; - 加载旧会话:
loadSession()同样重置为null(L376-L389)。如果用户把旧对话加载到新标签页,lastSentAppState为 null,全量上下文会重新发送一次——规格明确说明这是“fine and expected”; - 重试:
retryLastRequest()本身无需改动(L337-L355),它重新调用beginStreaming,会重新求值当前 app state,重试消息因此获得“新鲜”的上下文,这正是期望行为; - 即使未发送 header 也会更新
lastSentAppState:从源码看,persisted.update无条件写入当前状态,确保“上次已告知 Agent 的状态”始终与真实状态对齐,下一轮只需报告差异。
7. CI 测试:agentInfo 覆盖率静态扫描
为防止新增页面遗漏上下文声明,仓库内置了一个 Vitest 静态扫描测试 agent_coverage.test.ts:
function findPageFiles(dir: string): string[] { // 递归遍历,收集所有名为 +page.svelte 的文件 } describe("agentInfo coverage", () => { const routesDir = path.resolve(__dirname, "../routes") const pageFiles = findPageFiles(routesDir) it("should find page files", () => { expect(pageFiles.length).toBeGreaterThan(0) }) it.each(pageFiles.map((f) => [path.relative(routesDir, f), f]))( "%s contains agentInfo.set or agentInfo.update", (_relativePath, filePath) => { const content = fs.readFileSync(filePath as string, "utf-8") expect( content.includes("agentInfo.set(") || content.includes("agentInfo.update("), `${_relativePath} is missing an agentInfo.set() or agentInfo.update() call. This is required to inform our agent what this page is.`, ).toBe(true) }, ) })它与 project_overview.md 中给出的“rough idea”略有差异:最终实现用path.resolve(__dirname, "../routes")定位路由目录(比process.cwd()更稳健),并用it.each为每个页面生成独立用例——某个页面失败时 CI 输出能直接定位到具体文件。
8. 测试策略:单元 + 集成双层验证
全部测试基于 Vitest,分为两层:
单元测试 agent.test.ts
覆盖buildContextHeader/getCurrentAppState/formatHeader/formatChangedHeader:
lastSent = null(首条消息):返回包含全部非空字段的完整头;- 状态无变化:返回
null; - 部分变化:头中只含变化字段(断言
not.toContain("Current Project")等); - null 字段从完整头中省略;
- 仅
path变化时,即使其他字段为 null 也包含Path; - 字段从非 null 过渡到 null 时,以
(none)表示(如Current Task: (none))。
集成测试 chat_session_store.test.ts
"context header"describe 块通过 mock$lib/agent模块并捕获streamChat入参来验证注入链路:
- 首条消息时在 API 消息内容前拼接
<new_app_ui_context>,且保留用户原文; - 存入
messages的用户消息不含header(expect(userMsg?.content).toBe("hello")); buildContextHeader返回null时原样发送;- 发送后
lastSentAppState被写入 session storage; reset()后lastSentAppState归零;- 即使未发送 header 也会更新
lastSentAppState。
9. 实施阶段与小结
规格将实施划分为两个阶段(见 functional_spec.md 的 Phases 一节):
- Phase 1:核心基础设施——
agentInfostore、lastSentAppState追踪、上下文头前缀拼接逻辑与配套测试; - Phase 2:为所有既有页面回填
agentInfo.set()调用,并加上第 7 节的 CI 覆盖率测试。
配套的架构说明见 architecture.md,其中还记录了边界情况的设计考量:SSR 下的window保护、快速导航时取“最后一次设置的 agentInfo”(正好反映用户当前所看页面)、agentInfo为 null 时至少保证Path可用。
整体来看,这套机制体现了三个工程取舍:用增量 diff控制上下文体积;用存储与发送分离(干净文本入库、带头 payload 出站)保证 UI 体验不受影响;用静态 CI 扫描把“每个页面都要声明上下文”这一约定固化成不可绕过的检查。对于要在自有 SvelteKit 应用中为聊天 Agent 提供 UI 上下文的开发者,这是一份可以直接参照的实现模板:数据模型、注入时机、持久化重置语义与测试断言,均可在 agent.ts、chat_session_store.ts、agent_coverage.test.ts 中找到完整对应。
- 人工智能
- AI 应用
- AI 评测
- RAG
- 微调
- 提示工程
- 开发工具
- 桌面应用
【免费下载链接】Kiln
Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.
相关推荐
Kiln Chat App State:基于 Svelte Store 的 Agent 上下文头部注入机制全解析
Kiln Chat App State:基于 Svelte Store 的 Agent 上下文头部注入机制全解析 导读 Kiln 在桌面端与 Web 端引入了聊
人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOpsKiln 聊天上下文注入机制:用 `<new_app_ui_context>` 让 Agent 感知前端页面状态
Kiln 聊天上下文注入机制:用 <new_app_ui_context 让 Agent 感知前端页面状态 本文围绕 Kiln 开源仓库中 Chat App S
人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOpsKiln 聊天应用状态感知:基于 `<new_app_ui_context>` 上下文头的架构设计与实现解析
Kiln 聊天应用状态感知:基于 <new_app_ui_context 上下文头的架构设计与实现解析 本文以 Kiln 前端 Web UI(SvelteKit
人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOps
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考