news 2026/10/14 15:13:29

Kiln 聊天应用状态注入机制:Chat App State 的完整设计与源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kiln 聊天应用状态注入机制:Chat App State 的完整设计与源码解析
  • 人工智能
  • AI 应用
  • AI 评测
  • RAG
  • 微调
  • 提示工程
  • 开发工具
  • 桌面应用

【免费下载链接】Kiln

Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.

项目地址:https://gitcode.com/gh_mirrors/ki/Kiln
点击查看免费下载

本文围绕 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 设计):

  1. agentInfostore:每个 Svelte 页面声明自己的名称与描述;
  2. lastSentAppState追踪 + 上下文头拼接逻辑:在聊天会话 store 的发送流程中完成状态比对与注入;
  3. CI 覆盖率测试:静态扫描所有+page.svelte,确保新页面不会遗漏agentInfo声明。

2. App State 数据模型

应用状态由两个来源的字段组成,完整定义见 functional_spec.md:

字段来源示例
Path浏览器 URL 路径(window.location.pathname)/tools/189194447825/add_tools
Page NameagentInfostoreSettings Home
Page DescriptionagentInfostoreA page for managing user settings...
Current ProjectUIState store12345
Current TaskUIState store123456

在源码中,这一模型对应 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中注入

规格将发送逻辑概括为五步:

  1. 采集当前 app state:读agentInfo、UIState(project/task)与window.location.pathname;
  2. 从聊天会话 store 读取lastSentAppState(新会话为null);
  3. 逐字段与lastSentAppState比对;
  4. 若有字段变化(或lastSentAppState为null——首条消息):用仅含变化字段的块(首条消息则含全部字段)构建<new_app_ui_context>,前缀拼接到发给后端 API 的消息文本,并用完整当前状态更新lastSentAppState;
  5. 若无任何变化:原样发送消息,不加头。

实际落点在 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.

项目地址:https://gitcode.com/gh_mirrors/ki/Kiln
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 15:12:44

链队列的四大状态契约:语义正确性与资源可控性

1. 为什么链队列不是“链表队列”那么简单&#xff1f;“Python描述数据结构之链队列篇”——这个标题乍看平平无奇&#xff0c;像极了教材目录里被翻烂的一页。但我在某高校数据结构实训课带过三届学生后发现&#xff1a;92%的人能写出链表&#xff0c;85%的人能写出数组队列&…

作者头像 李华
网站建设 2026/10/14 15:12:40

商业与开源并行:解开腾讯WorkBuddy与Octop双布局之谜

最近我在技术社区里留意到一个很有意思的现象&#xff1a;腾讯的 WorkBuddy 刚在应用商店上线没多久&#xff0c;那边 Octop 开源的官方公告也发了出来&#xff0c;底下的评论直接炸了锅。很多人第一反应都是同一个问题——"腾讯是不是自己打自己&#xff1f;家里已经有了…

作者头像 李华
网站建设 2026/10/14 15:12:40

Java实战:用Gin Rummy游戏练手牌型识别与AI对战

简介&#xff1a;一份基于Java的经典杜松子酒接龙&#xff08;Gin Rummy&#xff09;纸牌游戏实现&#xff0c;遵循传统二人对战规则&#xff0c;玩家可与电脑AI进行完整对局&#xff0c;适合Java学习者、游戏开发爱好者及需要课程设计参考的高校学生。项目完整覆盖了发牌、出牌…

作者头像 李华
网站建设 2026/10/14 15:12:16

短视频旁白怎么写?适合AI配音的文案结构与写作技巧

同一段文字&#xff0c;放在文章里读起来很顺&#xff0c;转换成短视频旁白后却可能显得生硬。句子太长、信息太密、缺少停顿&#xff0c;都会影响AI配音的听感。如果平时使用文字转语音制作视频&#xff0c;写文案时就不能只考虑“看起来通顺”&#xff0c;还要考虑“听起来是…

作者头像 李华