docs: add technical architecture guide

This commit is contained in:
wuyang6
2026-05-18 19:28:02 +08:00
parent c4b935692c
commit 3054b3bc6b
53 changed files with 6692 additions and 2234 deletions
+368
View File
@@ -0,0 +1,368 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import type { Metadata } from "next";
import Image from "next/image";
import { notFound } from "next/navigation";
import type { ReactNode } from "react";
const technicalDocs = {
"01-base-runtime": {
title: "基座 Runtime",
file: "01-base-runtime.md",
image: "/doc-assets/technical/01-base-runtime.png",
},
"02-agent-loop": {
title: "Agent Loop",
file: "02-agent-loop.md",
image: "/doc-assets/technical/02-agent-loop.png",
},
"03-tools": {
title: "工具体系",
file: "03-tools.md",
image: "/doc-assets/technical/03-tools.png",
},
"04-skills": {
title: "Skill 体系",
file: "04-skills.md",
image: "/doc-assets/technical/04-skills.png",
},
"05-workspace-memory-observability": {
title: "工作区、运行态和记忆",
file: "05-workspace-memory-observability.md",
image: "/doc-assets/technical/05-workspace-memory-observability.png",
},
"06-product-data": {
title: "product-data",
file: "06-product-data.md",
image: "/doc-assets/technical/06-product-data.png",
},
"07-online-mining-v2": {
title: "online-mining-v2",
file: "07-online-mining-v2.md",
image: "/doc-assets/technical/07-online-mining-v2.png",
},
"08-label-master": {
title: "标签大师",
file: "08-label-master.md",
image: "/doc-assets/technical/08-label-master.png",
},
"09-external-skills": {
title: "外部系统 Skill",
file: "09-external-skills.md",
image: "/doc-assets/technical/09-external-skills.png",
},
} as const;
type TechnicalDocSlug = keyof typeof technicalDocs;
function getDoc(slug: string) {
if (slug in technicalDocs) {
return technicalDocs[slug as TechnicalDocSlug];
}
return null;
}
function markdownPath(file: string) {
return path.join(
process.cwd(),
"public",
"doc-assets",
"technical-docs",
file,
);
}
function normalizeMarkdown(markdown: string) {
return markdown
.replace(/^\s*#\s+.+\n+/, "")
.replaceAll("](assets/", "](/doc-assets/technical/");
}
function parseInline(text: string): ReactNode[] {
const nodes: ReactNode[] = [];
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[([^\]]+)\]\(([^)]+)\))/g;
let cursor = 0;
while (true) {
const match = pattern.exec(text);
if (match === null) break;
if (match.index > cursor) {
nodes.push(text.slice(cursor, match.index));
}
const token = match[0];
const key = `${match.index}-${token}`;
if (token.startsWith("`")) {
nodes.push(<code key={key}>{token.slice(1, -1)}</code>);
} else if (token.startsWith("**")) {
nodes.push(<strong key={key}>{token.slice(2, -2)}</strong>);
} else {
const href = match[3] ?? "";
const isExternal = /^https?:\/\//.test(href);
nodes.push(
<a
href={href}
key={key}
rel={isExternal ? "noopener" : undefined}
target={isExternal ? "_blank" : undefined}
>
{match[2]}
</a>,
);
}
cursor = pattern.lastIndex;
}
if (cursor < text.length) {
nodes.push(text.slice(cursor));
}
return nodes;
}
function splitTableRow(line: string) {
return line
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim());
}
function isTableDivider(line: string) {
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
}
function startsBlock(line: string, nextLine?: string) {
return (
line.startsWith("```") ||
/^#{1,6}\s+/.test(line) ||
/^[-*]\s+/.test(line) ||
/^\d+\.\s+/.test(line) ||
/^>\s?/.test(line) ||
/^!\[[^\]]*]\([^)]+\)\s*$/.test(line) ||
(line.includes("|") && nextLine !== undefined && isTableDivider(nextLine))
);
}
function renderHeading(depth: number, text: string, key: string) {
if (depth === 1) return <h1 key={key}>{parseInline(text)}</h1>;
if (depth === 2) return <h2 key={key}>{parseInline(text)}</h2>;
if (depth === 3) return <h3 key={key}>{parseInline(text)}</h3>;
if (depth === 4) return <h4 key={key}>{parseInline(text)}</h4>;
if (depth === 5) return <h5 key={key}>{parseInline(text)}</h5>;
return <h6 key={key}>{parseInline(text)}</h6>;
}
function renderMarkdown(markdown: string) {
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
const blocks: ReactNode[] = [];
let index = 0;
while (index < lines.length) {
const line = lines[index];
const trimmed = line.trim();
if (!trimmed) {
index += 1;
continue;
}
if (trimmed.startsWith("```")) {
const language = trimmed.slice(3).trim() || "text";
const codeLines: string[] = [];
index += 1;
while (index < lines.length && !lines[index].trim().startsWith("```")) {
codeLines.push(lines[index]);
index += 1;
}
if (index < lines.length) index += 1;
blocks.push(
<div className="project-doc-code-block" key={`code-${index}`}>
<div>{language}</div>
<pre>
<code>{codeLines.join("\n")}</code>
</pre>
</div>,
);
continue;
}
const heading = trimmed.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
blocks.push(
renderHeading(heading[1].length, heading[2], `heading-${index}`),
);
index += 1;
continue;
}
const image = trimmed.match(/^!\[([^\]]*)]\(([^)]+)\)\s*$/);
if (image) {
blocks.push(
<Image
alt={image[1]}
height={900}
key={`image-${index}`}
src={image[2]}
width={1600}
/>,
);
index += 1;
continue;
}
if (
trimmed.includes("|") &&
index + 1 < lines.length &&
isTableDivider(lines[index + 1])
) {
const headers = splitTableRow(trimmed);
index += 2;
const rows: string[][] = [];
while (index < lines.length && lines[index].trim().includes("|")) {
rows.push(splitTableRow(lines[index]));
index += 1;
}
blocks.push(
<table key={`table-${index}`}>
<thead>
<tr>
{headers.map((cell) => (
<th key={cell}>{parseInline(cell)}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.join("|")}>
{row.map((cell) => (
<td key={`${row.join("|")}-${cell}`}>{parseInline(cell)}</td>
))}
</tr>
))}
</tbody>
</table>,
);
continue;
}
if (/^[-*]\s+/.test(trimmed)) {
const items: string[] = [];
while (index < lines.length && /^[-*]\s+/.test(lines[index].trim())) {
items.push(lines[index].trim().replace(/^[-*]\s+/, ""));
index += 1;
}
blocks.push(
<ul key={`ul-${index}`}>
{items.map((item) => (
<li key={item}>{parseInline(item)}</li>
))}
</ul>,
);
continue;
}
if (/^\d+\.\s+/.test(trimmed)) {
const items: string[] = [];
while (index < lines.length && /^\d+\.\s+/.test(lines[index].trim())) {
items.push(lines[index].trim().replace(/^\d+\.\s+/, ""));
index += 1;
}
blocks.push(
<ol key={`ol-${index}`}>
{items.map((item) => (
<li key={item}>{parseInline(item)}</li>
))}
</ol>,
);
continue;
}
if (/^>\s?/.test(trimmed)) {
const quoteLines: string[] = [];
while (index < lines.length && /^>\s?/.test(lines[index].trim())) {
quoteLines.push(lines[index].trim().replace(/^>\s?/, ""));
index += 1;
}
blocks.push(
<blockquote key={`quote-${index}`}>
{parseInline(quoteLines.join(" "))}
</blockquote>,
);
continue;
}
const paragraphLines = [trimmed];
index += 1;
while (
index < lines.length &&
lines[index].trim() &&
!startsBlock(lines[index].trim(), lines[index + 1]?.trim())
) {
paragraphLines.push(lines[index].trim());
index += 1;
}
blocks.push(
<p key={`p-${index}`}>{parseInline(paragraphLines.join(" "))}</p>,
);
}
return blocks;
}
export function generateStaticParams() {
return Object.keys(technicalDocs).map((slug) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const doc = getDoc(slug);
if (!doc) return {};
return {
title: `${doc.title} - ZK Data Agent`,
};
}
export default async function TechnicalDocPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const doc = getDoc(slug);
if (!doc) notFound();
const rawMarkdown = await readFile(markdownPath(doc.file), "utf8");
const markdown = normalizeMarkdown(rawMarkdown);
return (
<main className="project-doc-page project-doc-detail-page">
<header className="project-doc-detail-hero">
<nav className="project-doc-nav">
<a href="/doc">ZK Data Agent</a>
<div>
<a href="/doc"></a>
</div>
</nav>
<div className="project-doc-detail-head">
<p className="project-doc-kicker"></p>
<h1>{doc.title}</h1>
<a className="project-doc-back-link" href="/doc">
</a>
</div>
</header>
<section className="project-doc-detail-shell">
<article className="project-doc-article">
{renderMarkdown(markdown)}
</article>
</section>
</main>
);
}
+339 -605
View File
@@ -1,589 +1,266 @@
"use client";
import Image from "next/image";
import { ToolPositionMap } from "./tool-position-map";
import {
ArrowRight,
Check,
CheckCircle2,
CircleDashed,
FolderTree,
Network,
Route,
Sparkles,
X,
} from "lucide-react";
import { useMemo, useState } from "react";
type RoleId = "user" | "builder" | "admin";
type ProductId = "chatgpt" | "cursor" | "claude" | "zk";
type MemoryId = "chatgpt" | "letta" | "mem0" | "rag" | "ours";
type SkillPartId = "skillmd" | "knowledge" | "scripts" | "examples" | "readme" | "update";
const roles: Record<
RoleId,
{
name: string;
title: string;
question: string;
answer: string;
workflow: string[];
proof: string[];
}
> = {
user: {
name: "普通使用者",
title: "我想让它替我完成一段工作",
question: "我为什么不用 ChatGPT 网页版问一问?",
answer:
"因为这里不是只回答一句话,而是把输入、计划、人工 review、脚本执行、文件产物和后续导出放进同一个任务空间。",
workflow: ["选择 Skill", "描述目标", "review 计划", "等待执行", "拿 output 文件"],
proof: ["会话文件面板", "右侧活动链路", "output/ 产物", "飞书在线文档转换"],
},
builder: {
name: "Skill 作者",
title: "我想把自己的经验变成团队能力",
question: "为什么不是每个人自己写脚本、自己维护 prompt?",
answer:
"Skill 把流程、知识、脚本、样例和维护说明放到一个目录。别人不用理解你的全部细节,也能在 Web 上调用这份能力。",
workflow: ["建 skill 目录", "写 SKILL.md", "沉淀 knowledge", "放 scripts", "提交 Git", "页面更新 Skill"],
proof: ["skills/product-data", "skills/online-mining-v2", "skills/label-master", "Skill 更新按钮"],
},
admin: {
name: "平台维护者",
title: "我想让多人稳定使用,而不是每个人本地各跑各的",
question: "为什么不用 Claude Code 本地跑?",
answer:
"Claude Code 很适合个人本地工程闭环;这里补的是团队 Web 入口、多用户会话、账号记忆、管理后台、共享 Skill 和统一部署。",
workflow: ["管理用户", "看会话与用量", "处理记忆队列", "更新服务", "维护公共 Skill"],
proof: ["/admin", "memory.db", "systemd 服务", "dev / online 环境", "会话隔离"],
},
type Section = {
id: string;
no: string;
title: string;
summary: string;
image?: string;
doc?: string;
points: string[];
code: string[];
decision: string;
};
const products: Record<
ProductId,
{
name: string;
position: string;
bestFor: string;
limits: string;
tech: string[];
}
> = {
chatgpt: {
name: "ChatGPT 网页版",
position: "最好的通用对话入口",
bestFor: "问答、写作、临时分析、个人信息辅助。",
limits: "不天然绑定团队数据源、产物目录、Skill 更新和多用户运维。",
tech: ["Saved Memory", "Reference Chat History", "Files", "Connectors"],
},
cursor: {
name: "Cursor",
position: "IDE 内的代码协作体验",
bestFor: "围绕代码仓库改文件、补全、解释、局部重构。",
limits: "偏个人 IDE 场景,业务流程共享、产物管理和多人 Web 入口不是核心。",
tech: ["Codebase Index", "Composer", "IDE Context", "Rules"],
},
claude: {
name: "Claude Code",
position: "本地工程 Agent 的强基准",
bestFor: "读代码、改文件、跑测试、长任务、多工具工程闭环。",
limits: "团队业务能力共建、账号级 Skill 记忆、Web 管理和数据开发产物链需要另搭平台。",
tech: ["Agent Loop", "Tools", "Skills", "MCP", "Terminal Workspace"],
},
zk: {
name: "ZK Data Agent",
position: "团队业务流程 Agent 工作台",
bestFor: "把数据开发、标签判断、线上挖掘、格式导出沉淀成共享 Skill。",
limits: "不追求替代所有 IDE/终端 Agent,优先服务团队业务流程沉淀。",
tech: ["Skill Registry", "Review Gate", "Session Workspace", "Skill Memory", "Admin"],
},
};
const assetPrefix = "/doc-assets/technical";
const architectureImage = `${assetPrefix}/00-zk-data-agent-architecture.png`;
const comparisonRows = [
{ label: "Web 多用户入口", chatgpt: "partial", cursor: "no", claude: "no", zk: "yes" },
{ label: "团队共享 Skill", chatgpt: "partial", cursor: "partial", claude: "partial", zk: "yes" },
{ label: "业务数据源接入", chatgpt: "partial", cursor: "no", claude: "partial", zk: "yes" },
{ label: "会话产物管理", chatgpt: "partial", cursor: "no", claude: "partial", zk: "yes" },
{ label: "人类 Review Gate", chatgpt: "partial", cursor: "partial", claude: "partial", zk: "yes" },
{ label: "Skill 级记忆", chatgpt: "no", cursor: "no", claude: "partial", zk: "yes" },
{ label: "管理后台/用量/队列", chatgpt: "no", cursor: "no", claude: "no", zk: "yes" },
{ label: "数据开发链路沉淀", chatgpt: "partial", cursor: "no", claude: "partial", zk: "yes" },
] as const;
const objectives = [
const sections: Section[] = [
{
goal: "先回答为什么用我们",
frontend: "用产品对比矩阵 + 选中产品详情,明确展示我们补的是团队流程平台能力。",
message: "不是模型比 Claude Code 强,而是把 Web 多用户、共享 Skill、产物管理、记忆和管理后台补齐。",
id: "base",
no: "01",
title: "基座 Runtime",
summary:
"基座把本地工程 Agent 的循环执行能力,包装成多人可使用、可观测、可沉淀、可扩展的 Web 工作台。",
image: `${assetPrefix}/01-base-runtime.png`,
doc: "/doc/01-base-runtime",
points: [
"基于 LocalCodingAgent、OpenAI-compatible 模型调用、Tool Runtime、Session Workspace 和 Skill System 组合而成。",
"解决个人脚本和一次性对话难以团队复用的问题:流程、工具、知识、产物都挂到同一个会话空间。",
"在 product-data、online-mining-v2、标签大师、飞书在线文档转换等场景中验证了基座的通用承载能力。",
],
code: [
"backend/api/server.py",
"src/agent_runtime.py",
"src/agent_prompting.py",
"src/openai_compat.py",
],
decision:
"基座只承载跨业务稳定能力;变化快的业务判断、流程经验和格式转换下沉到 Skill。",
},
{
goal: "让不同同事知道怎么融入工作",
frontend: "用角色切换:普通使用者、Skill 作者、平台维护者分别看到自己的路径。",
message: "看完不是只知道概念,而是知道自己明天怎么开始用。",
id: "loop",
no: "02",
title: "Agent Loop",
summary:
"模型不是只回答一次,而是在多轮循环里阅读上下文、请求工具、观察结果、继续判断。",
image: `${assetPrefix}/02-agent-loop.png`,
doc: "/doc/02-agent-loop",
points: [
"模型是否返回 tool_calls 由模型基于 messages、工具描述、Skill 和上下文自行决定。",
"工具结果写回 session,成为下一轮模型判断的输入。",
"review gate 不是特殊流程,而是一次运行自然暂停,等待用户下一轮确认后 resume。",
],
code: [
"LocalCodingAgent.run",
"LocalCodingAgent.resume",
"_run_prompt",
"_query_model",
],
decision:
"复杂任务需要执行后再判断;把工具观察结果纳入下一轮上下文,比一次性输出更可靠。",
},
{
goal: "证明不是闭门造车",
frontend: "用记忆方案谱系,把 ChatGPT、Letta、Mem0、RAG 和我们的取舍放在同一屏。",
message: "我们参考了主流方案,但按团队 Skill 场景做了轻量可控实现。",
id: "tools",
no: "03",
title: "工具体系",
summary:
"工具由 Tool spec 和 handler 两部分组成:spec 给模型选择,handler 在后端执行。",
image: `${assetPrefix}/03-tools.png`,
doc: "/doc/03-tools",
points: [
"Tool registry 汇总文件、执行、搜索、Skill、data_agent、MCP 等能力。",
"模型只看到 name、description 和参数 schema,看不到 handler 实现。",
"结构化分析和复杂文件写入优先走 python_execwrite_file 只适合小文件。",
],
code: [
"src/agent_tools.py",
"src/agent_tool_specs/files.py",
"src/agent_tool_specs/execution.py",
],
decision:
"模型负责选择能力,代码负责稳定执行能力;两者分开后才能做权限、取消、路径和审计。",
},
{
goal: "解释 Skill 为什么能沉淀能力",
frontend: "用可点击文件树解剖 SKILL.md、knowledge、scripts、examples、README 和更新机制。",
message: "Skill 不是 prompt,而是可维护、可安装、可更新的能力包。",
id: "skills",
no: "04",
title: "Skill 体系",
summary:
"Skill 是能力包,包含流程协议、领域知识、确定性脚本、样例和维护说明。",
image: `${assetPrefix}/04-skills.png`,
doc: "/doc/04-skills",
points: [
"SKILL.md 定义触发场景、执行流程、review 边界和产物约定。",
"knowledge/ 放业务定义和边界经验,scripts/ 放稳定脚本。",
"项目级 skills/ 可以通过页面更新机制刷新到模型可见能力列表。",
],
code: [
"src/bundled_skills.py",
"skills/*/SKILL.md",
"frontend Skills 面板",
],
decision:
"团队经验只有形成可安装、可更新、可阅读、可执行的目录结构,才方便多人复用和维护。",
},
{
goal: "让技术同事能追代码",
frontend: "每条链路都给出代码路径、落盘位置和真实运行节点。",
message: "页面不是海报,是技术讲解和后续维护入口。",
id: "workspace",
no: "05",
title: "工作区、运行态和记忆",
summary:
"每个账号和会话都有独立空间,运行过程通过事件流展示,交互结束后异步整理记忆。",
image: `${assetPrefix}/05-workspace-memory-observability.png`,
doc: "/doc/05-workspace-memory-observability",
points: [
"session/input 放输入材料,session/scratchpad 放中间过程,session/output 放最终产物。",
"右侧活动区和摘要行来自运行态事件流,刷新后通过会话记录恢复。",
"用户记忆和 Skill 记忆分开保存,只在需要的上下文中注入。",
],
code: [
".port_sessions/accounts/{account}/sessions/{session}",
"RunStateStore",
"src/personal_memory.py",
],
decision: "多人使用时,隔离、恢复、取消、观测和产物管理比单次回答更重要。",
},
];
const memoryPatterns: Record<
MemoryId,
const skillSections: Section[] = [
{
name: string;
source: string;
borrowed: string;
notEnough: string;
ourChoice: string;
docs: string;
href: string;
}
> = {
chatgpt: {
name: "ChatGPT Saved Memory / Reference Chat History",
source: "OpenAI 的记忆分成显式 Saved Memories 和从历史中提炼的 Reference Chat History。",
borrowed: "借鉴:用户可管理、可删除、可开关;显式记忆和历史参考分层。",
notEnough: "不足:它面向通用个性化,不知道某个团队 Skill 的操作经验应该只在该 Skill 生效。",
ourChoice: "我们的取舍:用户记忆全局注入,Skill 使用记忆只在启用对应 Skill 时注入。",
docs: "OpenAI Memory FAQ",
href: "https://help.openai.com/en/articles/8590148-memory-faq",
},
letta: {
name: "Letta / MemGPT Core + Archival Memory",
source: "Letta 把 stateful agent 的消息、工具调用、记忆持久化,并区分 in-context memory blocks 和 archival memory。",
borrowed: "借鉴:核心记忆常驻上下文,外部记忆通过工具或检索进入;记忆是 Agent state 的一部分。",
notEnough: "不足:完整自管理记忆体系复杂,早期直接照搬会增加主链路风险和运维成本。",
ourChoice: "我们的取舍:不让主 Agent 实时自改记忆,而是把交互事件异步交给后台 worker 整理。",
docs: "Letta Stateful Agents / Archival Memory",
href: "https://docs.letta.com/guides/core-concepts/stateful-agents",
},
mem0: {
name: "Mem0 / Managed Memory Layer",
source: "Mem0 把用户、Agent、Session 记忆做成可托管的长期记忆层,并支持 add/search/update/delete。",
borrowed: "借鉴:每次交互后抽取记忆;调用前按用户请求检索并注入。",
notEnough: "不足:我们当前更需要可审阅、可编辑、可随项目部署的轻量实现,而不是先接托管记忆服务。",
ourChoice: "我们的取舍:SQLite 做事件队列,Markdown 做人可编辑记忆正文,后续可替换成 Mem0/向量服务。",
docs: "Mem0 Platform Overview",
href: "https://docs.mem0.ai/platform/overview",
},
rag: {
name: "RAG / Vector Memory",
source: "把资料切片、embedding、向量检索,再把相关片段注入上下文。",
borrowed: "借鉴:大规模知识不应全部塞进 prompt,要按任务召回。",
notEnough: "不足:Skill 使用经验通常是流程偏好和纠错结论,不是纯语义相似文本召回。",
ourChoice: "我们的取舍:业务知识先放 skill/knowledge;记忆先用结构化 Markdown,后续知识膨胀再接召回索引。",
docs: "Letta Archival Memory",
href: "https://docs.letta.com/guides/ade/archival-memory",
},
ours: {
name: "ZK Data Agent Memory",
source: "结合团队 Skill 场景:用户偏好和 Skill 使用经验是两类不同记忆。",
borrowed: "借鉴:显式记忆、历史抽取、长期持久化、用户可编辑、异步后台处理。",
notEnough: "主动放弃:不做实时自修改、不强依赖向量库、不把所有历史都注入。",
ourChoice: "最终形态:user.md + skills/*.md + memory.db queue + worker + enabled skill 注入。",
docs: "本项目实现",
href: "#memory-implementation",
},
};
const memoryPipeline = [
{ title: "检测信号", text: "显式记住 / 纠错 / Skill 流程 / 工具失败", code: "detect_memory_signals" },
{ title: "事件入队", text: "主链路只写 pending event,不同步整理", code: "enqueue_interaction" },
{ title: "批量整理", text: "后台 worker 每轮最多取 8 条,按 priority 处理", code: "_process_account_events" },
{ title: "LLM 合并", text: "合并旧记忆,不追加流水账;失败走规则 fallback", code: "_generate_memory_updates" },
{ title: "Markdown 落盘", text: "用户可看、可改、可删;revision 可追踪", code: "user.md / skills/*.md" },
{ title: "按需注入", text: "只注入用户记忆和当前启用 Skill 的记忆", code: "render_injection" },
];
const skills: Record<
SkillPartId,
{
name: string;
path: string;
purpose: string;
agentSees: string;
maintainerDoes: string;
}
> = {
skillmd: {
name: "SKILL.md",
path: "skills/<name>/SKILL.md",
purpose: "定义 Agent 如何识别任务、何时澄清、何时 review、何时调用脚本。",
agentSees: "流程协议、工具优先级、产物约定、人类确认边界。",
maintainerDoes: "把经验写成可执行步骤,而不是把所有业务细节塞成一大段 prompt。",
},
knowledge: {
name: "knowledge/",
path: "skills/<name>/knowledge/",
purpose: "放业务定义、标签边界、字段说明、判断准则。",
agentSees: "按需读取具体知识文件,用于分析和生成。",
maintainerDoes: "把经常变的业务规则放这里,降低改 SKILL.md 的频率。",
},
scripts: {
name: "scripts/",
path: "skills/<name>/scripts/",
purpose: "放确定性能力:校验、格式转换、导出、数据清洗。",
agentSees: "通过 python_exec 执行脚本,不再现场手写复杂转换逻辑。",
maintainerDoes: "把易错代码沉淀成可测试脚本,输入输出写清楚。",
},
examples: {
name: "examples/",
path: "skills/<name>/examples/",
purpose: "放典型输入输出,帮助 Agent 对齐格式和边界。",
agentSees: "少量高质量样例,比长篇抽象说明更稳。",
maintainerDoes: "新增失败 case 的最小复现,作为回归材料。",
},
readme: {
name: "README.md",
path: "skills/<name>/README.md",
purpose: "给人看的维护说明:这是什么、怎么测、怎么扩展。",
agentSees: "通常不强制注入,但可被读取用于理解项目背景。",
maintainerDoes: "保证同事能接手,不必找原作者口头说明。",
},
update: {
name: "更新 Skill",
path: "Web 会话页 > Skills > 更新 Skill",
purpose: "同事提交 Git 后,页面内拉取最新 Skill 文件,刷新可用能力。",
agentSees: "更新后的 Skill 列表会进入模型可见能力列表。",
maintainerDoes: "提交 Skill 改动后提醒使用者点更新;平台发布时再统一收敛。",
},
};
const taskFlows = [
{
name: "产品定义 → 数据集",
skill: "product-data",
input: "产品文档 / 手写标签边界 / 示例 query",
review: "目标标签、complex、覆盖范围、数量计划",
outputs: ["records.jsonl", "review.csv", "train.jsonl", "eval.csv"],
id: "product",
no: "06",
title: "product-data",
summary:
"从产品定义、标签边界、样例 query 或 badcase 出发,生成标准 canonical records 并导出多种数据格式。",
image: `${assetPrefix}/06-product-data.png`,
doc: "/doc/06-product-data",
points: [
"先抽取 generation goal,再让用户 review 标签、complex、覆盖范围和排除边界。",
"确认后生成 generation plan,再确认数量、轮次、路径和导出目标。",
"dataset draft text 通过 scripts 转成 canonical records,再导出 records.jsonl、流转 CSV、训练 JSONL、评测 CSV。",
],
code: [
"skills/product-data/SKILL.md",
"skills/product-data/scripts/*",
"canonical_record_v1",
],
decision:
"数据生成质量不只取决于模型文本,还取决于格式规范、校验脚本和 review 门禁。",
},
{
name: "线上日志 → 专项样本",
skill: "online-mining-v2 + elk-fetch",
input: "线上 badcase / query 特征 / domain 条件 / ELK 表",
review: "筛选策略、候选质量、是否直接转样本还是继续生成",
outputs: ["candidates.csv", "sample_review.md", "records.jsonl"],
id: "online",
no: "07",
title: "online-mining-v2",
summary:
"基于 ELK 线上日志挖掘候选样本,支持策略 review、抽样分析,以及直接转换为标准数据。",
image: `${assetPrefix}/07-online-mining-v2.png`,
doc: "/doc/07-online-mining-v2",
points: [
"从需求、badcase 或示例 query 分析查询策略。",
"通过 ELK 表获取 req_id、session、模型 prompt、模型输出、domain 等字段。",
"候选结果先抽样 review,再决定直接转样本还是进入 product-data 补数链路。",
],
code: [
"skills/online-mining-v2/SKILL.md",
"skills/elk-fetch/",
"build_online_records.py",
],
decision: "线上挖掘不是一次查询,策略需要多轮调整,候选质量需要人工确认。",
},
{
name: "标签定义 → 判断辅助",
skill: "标签大师",
input: "query / 多轮上下文 / 标签定义知识库",
review: "复杂度、多指令、自动任务、业务标签、输出形态",
outputs: ["label decision", "boundary evidence", "target function"],
id: "label",
no: "08",
title: "标签大师",
summary:
"把标签定义、复杂度、多指令、自动任务和边界经验组织为可检索、可解释的知识体系。",
image: `${assetPrefix}/08-label-master.png`,
doc: "/doc/08-label-master",
points: [
"复杂度、标签、多指令、自动任务是不同判断维度。",
"标签知识通过目录、索引、边界卡片和 manifest 组织。",
"输出需要同时满足标签合法性、输出形态和 complex 独立字段要求。",
],
code: [
"skills/label-master/knowledge",
"build_label_manifest.py",
"validate_label_output.py",
],
decision:
"标签判断需要逐步分析和引用知识,而不是把所有定义塞进一次模型输入。",
},
{
id: "external",
no: "09",
title: "外部系统 Skill",
summary:
"ELK、SQL、模型迭代、飞书文档等外部能力通过 Skill 包方式接入平台。",
image: `${assetPrefix}/09-external-skills.png`,
doc: "/doc/09-external-skills",
points: [
"SKILL.md 描述外部系统能力边界和使用流程。",
"scripts/ 负责调用外部 API、鉴权、查询和格式转换。",
"最终结果统一回到当前会话 output,而不是散落在项目根目录。",
],
code: [
"skills/elk-fetch",
"skills/data-factory-sql",
"skills/model-iteration",
"Feishu MCP 转在线文档",
],
decision: "外部系统能力变化快,适合跟随 Skill 独立迭代,避免侵入基座。",
},
];
const runtimeTrace = [
{ stage: "前端发送", text: "携带 account/session/model/enabled skills", path: "app/api/chat/route.ts" },
{ stage: "上下文拼接", text: "Skill 列表 + 用户记忆 + Skill 记忆 + workspace", path: "backend/api/server.py" },
{ stage: "Agent Loop", text: "模型决定回复、调用工具、执行脚本或等待 review", path: "src/agent_runtime.py" },
{ stage: "工具执行", text: "python_exec / file / search / MCP / Skill scripts", path: "src/agent_tools.py" },
{ stage: "事件流", text: "tool_start、tool_end、content_delta、tool_delta", path: "RunStateStore" },
{ stage: "产物落盘", text: "scratchpad 存中间过程,output 存最终文件", path: ".port_sessions/.../sessions" },
{ stage: "后台记忆", text: "交互完成后异步整理长期经验", path: "src/personal_memory.py" },
];
const sourceLinks = [
{ label: "OpenAI Memory FAQ", href: "https://help.openai.com/en/articles/8590148-memory-faq" },
{ label: "Letta Stateful Agents", href: "https://docs.letta.com/guides/core-concepts/stateful-agents" },
{ label: "Letta Archival Memory", href: "https://docs.letta.com/guides/ade/archival-memory" },
{ label: "Mem0 Overview", href: "https://docs.mem0.ai/platform/overview" },
{ label: "Semantic Kernel + Mem0", href: "https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-memory" },
];
function StatusIcon({ value }: { value: "yes" | "partial" | "no" }) {
if (value === "yes") return <Check className="doc-status yes" />;
if (value === "partial") return <CircleDashed className="doc-status partial" />;
return <X className="doc-status no" />;
}
function ProductComparison() {
const [selected, setSelected] = useState<ProductId>("zk");
const detail = products[selected];
const productIds: ProductId[] = ["chatgpt", "cursor", "claude", "zk"];
function SectionBlock({ section }: { section: Section }) {
return (
<section className="doc-section doc-compare-lab" id="why-us">
<div className="doc-section-heading">
<p className="doc-kicker"></p>
<h2> Claude Code / ChatGPT / Cursor </h2>
<p>
Skill
</p>
<section className="project-doc-section" id={section.id}>
<div className="project-doc-section-head">
<span>{section.no}</span>
<div>
<h2>{section.title}</h2>
<p>{section.summary}</p>
</div>
</div>
<div className="doc-compare-layout">
<div className="doc-product-switcher">
{productIds.map((id) => (
<button
className={selected === id ? "is-selected" : ""}
key={id}
onClick={() => setSelected(id)}
type="button"
>
<span>{products[id].name}</span>
<small>{products[id].position}</small>
</button>
{section.image ? (
<a
className="project-doc-figure"
href={section.image}
rel="noopener"
target="_blank"
>
<Image
alt={section.title}
height={900}
priority={section.id === "base"}
src={section.image}
width={1600}
/>
</a>
) : null}
<div className="project-doc-grid">
<div>
<h3></h3>
{section.points.map((point) => (
<p className="project-doc-point" key={point}>
{point}
</p>
))}
</div>
<div className="doc-product-card">
<p className="doc-kicker"></p>
<h3>{detail.name}</h3>
<div className="doc-product-facts">
<div>
<span></span>
<p>{detail.bestFor}</p>
</div>
<div>
<span></span>
<p>{detail.limits}</p>
</div>
</div>
<div className="doc-chip-list">
{detail.tech.map((item) => (
<span className="doc-chip" key={item}>{item}</span>
))}
</div>
</div>
</div>
<div className="doc-matrix">
<div className="doc-matrix-head">
<span></span>
{productIds.map((id) => <strong key={id}>{products[id].name}</strong>)}
</div>
{comparisonRows.map((row) => (
<div className="doc-matrix-row" key={row.label}>
<span>{row.label}</span>
{productIds.map((id) => (
<div className={selected === id ? "is-highlighted" : ""} key={id}>
<StatusIcon value={row[id]} />
</div>
))}
</div>
))}
</div>
</section>
);
}
function RoleSwitcher() {
const [selected, setSelected] = useState<RoleId>("user");
const role = roles[selected];
return (
<section className="doc-section doc-role-lab" id="roles">
<div className="doc-section-heading">
<p className="doc-kicker"></p>
<h2></h2>
</div>
<div className="doc-role-grid">
<div className="doc-role-tabs">
{(Object.keys(roles) as RoleId[]).map((id) => (
<button
className={selected === id ? "is-selected" : ""}
key={id}
onClick={() => setSelected(id)}
type="button"
>
{roles[id].name}
</button>
<div>
<h3></h3>
{section.code.map((item) => (
<code key={item}>{item}</code>
))}
{section.doc ? (
<a className="project-doc-link" href={section.doc} rel="noopener">
</a>
) : null}
</div>
<div className="doc-role-panel">
<h3>{role.title}</h3>
<div className="doc-role-question">{role.question}</div>
<p>{role.answer}</p>
<div className="doc-flow-chips">
{role.workflow.map((step) => (
<span key={step}>{step}</span>
))}
</div>
<div>
<h3></h3>
<p>{section.decision}</p>
</div>
<div className="doc-proof-panel">
<p className="doc-kicker"></p>
{role.proof.map((item) => (
<div className="doc-proof-item" key={item}>
<CheckCircle2 />
<span>{item}</span>
</div>
))}
</div>
</div>
</section>
);
}
function ObjectiveBoard() {
return (
<section className="doc-section doc-objectives" id="design-goals">
<div className="doc-section-heading">
<p className="doc-kicker"></p>
<h2></h2>
</div>
<div className="doc-objective-grid">
{objectives.map((item, index) => (
<article className="doc-objective" key={item.goal}>
<span>{String(index + 1).padStart(2, "0")}</span>
<h3>{item.goal}</h3>
<p>{item.frontend}</p>
<strong>{item.message}</strong>
</article>
))}
</div>
</section>
);
}
function MemoryAtlas() {
const [selected, setSelected] = useState<MemoryId>("ours");
const item = memoryPatterns[selected];
const order: MemoryId[] = ["chatgpt", "letta", "mem0", "rag", "ours"];
return (
<section className="doc-section doc-memory-lab" id="memory">
<div className="doc-section-heading">
<p className="doc-kicker"></p>
<h2></h2>
<p>
Agent Skill
</p>
</div>
<div className="doc-memory-map">
<div className="doc-memory-rail">
{order.map((id, index) => (
<button
className={selected === id ? "is-selected" : ""}
key={id}
onClick={() => setSelected(id)}
type="button"
>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{memoryPatterns[id].name}</strong>
</button>
))}
</div>
<div className="doc-memory-detail-v2">
<div className="doc-memory-source">
<a href={item.href}>{item.docs}</a>
</div>
<h3>{item.name}</h3>
<div className="doc-memory-grid-v2">
<div><span></span><p>{item.source}</p></div>
<div><span></span><p>{item.borrowed}</p></div>
<div><span></span><p>{item.notEnough}</p></div>
<div><span></span><p>{item.ourChoice}</p></div>
</div>
</div>
</div>
<div className="doc-memory-implementation" id="memory-implementation">
<div className="doc-section-heading compact">
<p className="doc-kicker"></p>
<h2> + Skill + </h2>
</div>
<div className="doc-memory-pipeline-v2">
{memoryPipeline.map((step, index) => (
<div className="doc-memory-step" key={step.title}>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{step.title}</strong>
<p>{step.text}</p>
<code>{step.code}</code>
</div>
))}
</div>
</div>
</section>
);
}
function SkillWorkbench() {
const [selected, setSelected] = useState<SkillPartId>("skillmd");
const part = skills[selected];
const order: SkillPartId[] = ["skillmd", "knowledge", "scripts", "examples", "readme", "update"];
return (
<section className="doc-section doc-skill-workbench" id="skill-dev">
<div className="doc-section-heading">
<p className="doc-kicker">Skill </p>
<h2></h2>
</div>
<div className="doc-skill-workbench-grid">
<div className="doc-file-tree-v2">
{order.map((id) => (
<button className={selected === id ? "is-selected" : ""} key={id} onClick={() => setSelected(id)} type="button">
<FolderTree />
<span>{skills[id].name}</span>
</button>
))}
</div>
<div className="doc-skill-part">
<code>{part.path}</code>
<h3>{part.name}</h3>
<div className="doc-skill-part-grid">
<div><span></span><p>{part.purpose}</p></div>
<div><span>Agent </span><p>{part.agentSees}</p></div>
<div><span></span><p>{part.maintainerDoes}</p></div>
</div>
</div>
<div className="doc-skill-steps">
<h3> Skill </h3>
{["创建目录", "补 SKILL.md", "沉淀脚本/知识", "本地测试", "提交 Git", "页面点更新 Skill"].map((step, index) => (
<div key={step}><span>{index + 1}</span>{step}</div>
))}
</div>
</div>
</section>
);
}
function TaskFlowMap() {
return (
<section className="doc-section doc-task-map" id="tasks">
<div className="doc-section-heading">
<p className="doc-kicker"></p>
<h2></h2>
</div>
<div className="doc-task-grid">
{taskFlows.map((flow) => (
<article className="doc-task-card" key={flow.name}>
<div className="doc-task-title"><Route /><h3>{flow.name}</h3></div>
<div className="doc-task-meta"><span>Skill</span><strong>{flow.skill}</strong></div>
<div className="doc-task-meta"><span></span><p>{flow.input}</p></div>
<div className="doc-task-meta"><span>Review </span><p>{flow.review}</p></div>
<div className="doc-output-list">
{flow.outputs.map((output) => <span key={output}>{output}</span>)}
</div>
</article>
))}
</div>
</section>
);
}
function RuntimeTrace() {
return (
<section className="doc-section doc-runtime-trace" id="runtime">
<div className="doc-section-heading">
<p className="doc-kicker"></p>
<h2></h2>
</div>
<div className="doc-runtime-rail">
{runtimeTrace.map((step, index) => (
<div className="doc-runtime-node" key={step.stage}>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{step.stage}</strong>
<p>{step.text}</p>
<code>{step.path}</code>
</div>
))}
</div>
</section>
);
@@ -591,62 +268,119 @@ function RuntimeTrace() {
export default function DocPage() {
return (
<main className="doc-page doc-page-v2">
<header className="doc-hero-v2">
<nav className="doc-nav">
<a className="doc-brand" href="/">
<span className="doc-brand-mark">ZK</span>
<span>Data Agent</span>
</a>
<div className="doc-nav-links">
<a href="#why-us"></a>
<a href="#roles"></a>
<a href="#memory"></a>
<a href="#skill-dev"> Skill</a>
<a href="#runtime"></a>
<main className="project-doc-page">
<header className="project-doc-hero">
<nav className="project-doc-nav">
<a href="/">ZK Data Agent</a>
<div>
<a href="#base"></a>
<a href="#loop">Agent Loop</a>
<a href="#skills">Skill</a>
<a href="#product"></a>
<a href="#code-index"></a>
</div>
</nav>
<section className="doc-hero-v2-grid">
<div className="doc-hero-v2-copy">
<p className="doc-kicker"></p>
<h1> Skill Agent </h1>
<p>
Skill线Skill
</p>
<div className="doc-hero-actions">
<a className="doc-primary-link" href="#why-us"> <ArrowRight /></a>
<a className="doc-secondary-link" href="#skill-dev"> Skill</a>
</div>
</div>
<div className="doc-command-wall">
<div className="doc-command-head"><Sparkles /> </div>
{["选择 product-data Skill", "对齐标签和 complex", "生成可 review 计划", "调用 scripts 导出 records/train/eval", "文件进入当前会话 output"].map((item, index) => (
<div className="doc-command-row" key={item}>
<span>{String(index + 1).padStart(2, "0")}</span>
<strong>{item}</strong>
</div>
))}
</div>
</section>
<div className="project-doc-hero-inner">
<p className="project-doc-kicker"></p>
<h1>ZK Data Agent</h1>
<p>
AgentSkill
线
</p>
</div>
</header>
<ObjectiveBoard />
<ProductComparison />
<RoleSwitcher />
<TaskFlowMap />
<MemoryAtlas />
<SkillWorkbench />
<RuntimeTrace />
<ToolPositionMap />
<section className="doc-section doc-source-links">
<div className="doc-section-heading">
<p className="doc-kicker"></p>
<h2> Agent </h2>
<section className="project-doc-section" id="architecture">
<div className="project-doc-section-head">
<span>00</span>
<div>
<h2>ZK-Data-Agent </h2>
<p>
Code Agent Loop
</p>
</div>
</div>
<div className="doc-source-grid">
{sourceLinks.map((link) => (
<a href={link.href} key={link.href}><Network />{link.label}</a>
))}
<a
className="project-doc-figure"
href={architectureImage}
rel="noopener"
target="_blank"
>
<Image
alt="ZK-Data-Agent 架构"
height={900}
priority
src={architectureImage}
width={1600}
/>
</a>
<div className="project-doc-grid">
<div>
<h3></h3>
<p>
Code Agent
</p>
</div>
<div>
<h3></h3>
<p>
ZK-Data-Agent Skill Skill Skill portable scripts
</p>
</div>
<div>
<h3></h3>
<p>
Skill Git Skill
</p>
</div>
</div>
</section>
{sections.map((section) => (
<SectionBlock key={section.id} section={section} />
))}
{skillSections.map((section) => (
<SectionBlock key={section.id} section={section} />
))}
<section className="project-doc-section" id="code-index">
<div className="project-doc-section-head">
<span>I</span>
<div>
<h2></h2>
<p>
Skill
</p>
</div>
</div>
<div className="project-doc-index">
<div>
<h3>Runtime</h3>
<code>backend/api/server.py</code>
<code>src/agent_runtime.py</code>
<code>src/openai_compat.py</code>
</div>
<div>
<h3>Prompt / Tool</h3>
<code>src/agent_prompting.py</code>
<code>src/agent_tools.py</code>
<code>src/agent_tool_specs/</code>
</div>
<div>
<h3>Skill</h3>
<code>src/bundled_skills.py</code>
<code>skills/product-data/</code>
<code>skills/label-master/</code>
</div>
<div>
<h3>State</h3>
<code>src/personal_memory.py</code>
<code>.port_sessions/accounts/</code>
<code>frontend/app/app/doc/page.tsx</code>
</div>
</div>
</section>
</main>
+566
View File
@@ -0,0 +1,566 @@
"use client";
import {
type CSSProperties,
type PointerEvent,
useMemo,
useRef,
useState,
} from "react";
type ToolPosition = {
id: string;
name: string;
x: number;
y: number;
z: number;
type: string;
color: string;
summary: string;
fit: string;
limit: string;
};
type Point3D = {
x: number;
y: number;
z: number;
};
type ProjectionMode = "xy" | "xz" | "yz" | "free";
const tools: ToolPosition[] = [
{
id: "manual",
name: "纯人工",
x: 24,
y: 82,
z: 22,
type: "人主导",
color: "#64748b",
summary: "最懂业务,但查、写、跑、改、整理都靠人推进。",
fit: "复杂边界判断、探索性分析、最终质量把关。",
limit: "执行成本高,流程复用依赖个人习惯和文档质量。",
},
{
id: "web-ai",
name: "网页版 AI",
x: 54,
y: 24,
z: 18,
type: "通用 AI",
color: "#38bdf8",
summary: "适合问答、总结、生成草稿,启动快。",
fit: "通用解释、文本生成、局部判断辅助。",
limit: "不天然接入内部日志、标签规则、标准产物和团队流程。",
},
{
id: "copilot",
name: "IDE Copilot",
x: 62,
y: 34,
z: 28,
type: "编码辅助",
color: "#60a5fa",
summary: "在编辑器里提升局部编码效率。",
fit: "补全、局部函数、单文件修改。",
limit: "更偏代码片段,不负责跨系统数据流和业务产物链。",
},
{
id: "cursor",
name: "Cursor",
x: 76,
y: 46,
z: 38,
type: "IDE Agent",
color: "#818cf8",
summary: "围绕代码仓库做理解、修改和多文件协作。",
fit: "工程代码迭代、仓库内上下文开发。",
limit: "团队业务流程、线上数据、标准导出和共享 Skill 需要另行组织。",
},
{
id: "code-agent",
name: "Claude Code / Codex",
x: 84,
y: 58,
z: 45,
type: "工程 Agent",
color: "#a78bfa",
summary: "能读代码、调工具、执行多步工程任务。",
fit: "本地工程闭环、复杂代码修改、命令执行。",
limit: "强在个人工作区;团队级业务流程沉淀和 Web 多用户协作不是默认形态。",
},
{
id: "notebook",
name: "个人自动化脚本",
x: 66,
y: 74,
z: 48,
type: "个人自动化",
color: "#f59e0b",
summary: "贴近业务数据,能把部分流程脚本化。",
fit: "数据清洗、临时统计、可重复实验。",
limit: "入口、权限、review、产物管理和团队复用通常需要人额外维护。",
},
{
id: "prompt-doc",
name: "经验文档",
x: 38,
y: 62,
z: 60,
type: "经验沉淀",
color: "#10b981",
summary: "能记录规则和经验,便于传播。",
fit: "标签边界、流程说明、操作规范。",
limit: "本身不执行任务,仍需要人把文档转成操作。",
},
{
id: "zk",
name: "ZK Data Agent",
x: 80,
y: 88,
z: 86,
type: "业务 Agent 工作台",
color: "#2563eb",
summary:
"把 Agent Loop、工具执行、业务知识、review 和产物管理组织成团队流程。",
fit: "数据生成、线上挖掘、标签判断、外部系统 Skill 接入。",
limit: "不替代人的业务判断;重点是把判断节点放进可复用流程。",
},
];
const presets: Array<{
mode: ProjectionMode;
name: string;
rotateX: number;
rotateY: number;
}> = [
{ mode: "xy", name: "研发 × 业务", rotateX: 0, rotateY: 0 },
{ mode: "xz", name: "研发 × 复用", rotateX: -90, rotateY: 0 },
{ mode: "yz", name: "业务 × 复用", rotateX: 0, rotateY: -90 },
];
const dimensionCards = [
{
title: "研发执行效率",
text: "读代码、调工具、跑脚本、生成产物。",
},
{
title: "业务嵌入深度",
text: "接住日志、标签体系、数据格式和团队规则。",
},
{
title: "流程可复用度",
text: "把一次解决方案沉淀成团队能继续调用的能力。",
},
];
const frictionCases = [
{ text: "标签边界依赖经验", weight: 5 },
{ text: "格式手工补齐", weight: 4 },
{ text: "规则散在文档", weight: 4 },
{ text: "线上数据难取", weight: 4 },
{ text: "经验靠口口相传", weight: 3 },
{ text: "脚本只在个人机器", weight: 3 },
{ text: "Review 断在中间", weight: 3 },
{ text: "样例难复用", weight: 2 },
{ text: "产物路径分散", weight: 2 },
{ text: "路径和权限反复确认", weight: 2 },
];
const cubeSize = 360;
const cubeCenter = cubeSize / 2;
const stageCenter = 280;
const perspective = 950;
const axisOrigin = { x: -cubeCenter, y: cubeCenter, z: -cubeCenter };
const axisDefinitions = [
{
id: "x",
name: "研发执行效率",
color: "#2563eb",
start: axisOrigin,
end: { x: cubeCenter, y: cubeCenter, z: -cubeCenter },
},
{
id: "y",
name: "业务嵌入深度",
color: "#0f766e",
start: axisOrigin,
end: { x: -cubeCenter, y: -cubeCenter, z: -cubeCenter },
},
{
id: "z",
name: "流程可复用度",
color: "#9333ea",
start: axisOrigin,
end: { x: -cubeCenter, y: cubeCenter, z: cubeCenter },
},
];
const visibleAxesByMode: Record<ProjectionMode, string[]> = {
xy: ["x", "y"],
xz: ["x", "z"],
yz: ["z", "y"],
free: ["x", "y", "z"],
};
function score(tool: ToolPosition) {
return Math.round((tool.x + tool.y + tool.z) / 3);
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
function getPointCoordinates(tool: ToolPosition) {
return {
x: (tool.x / 100) * cubeSize - cubeCenter,
y: cubeCenter - (tool.y / 100) * cubeSize,
z: (tool.z / 100) * cubeSize - cubeCenter,
};
}
function projectPoint(
point: Point3D,
rotateX: number,
rotateY: number,
mode: ProjectionMode,
) {
if (mode === "xy") {
return {
x: stageCenter + point.x,
y: stageCenter + point.y,
z: point.z,
scale: 1,
};
}
if (mode === "xz") {
return {
x: stageCenter + point.x,
y: stageCenter - point.z,
z: -point.y,
scale: 1,
};
}
if (mode === "yz") {
return {
x: stageCenter + point.z,
y: stageCenter + point.y,
z: point.x,
scale: 1,
};
}
const angleX = (rotateX * Math.PI) / 180;
const angleY = (rotateY * Math.PI) / 180;
const cosX = Math.cos(angleX);
const sinX = Math.sin(angleX);
const cosY = Math.cos(angleY);
const sinY = Math.sin(angleY);
const afterY = {
x: point.x * cosY + point.z * sinY,
y: point.y,
z: -point.x * sinY + point.z * cosY,
};
const rotated = {
x: afterY.x,
y: afterY.y * cosX - afterY.z * sinX,
z: afterY.y * sinX + afterY.z * cosX,
};
const scale = perspective / (perspective - rotated.z);
return {
x: stageCenter + rotated.x * scale,
y: stageCenter + rotated.y * scale,
z: rotated.z,
scale,
};
}
function toolAnchorStyle(
tool: ToolPosition,
projected: ReturnType<typeof projectPoint>,
) {
const scale = Math.min(1.08, Math.max(0.88, projected.scale));
return {
"--tool-x": `${projected.x}px`,
"--tool-y": `${projected.y}px`,
"--label-scale": `${scale}`,
"--point-color": tool.color,
zIndex: Math.round(500 + projected.z),
} as CSSProperties;
}
function getProjectedAxes(
rotateX: number,
rotateY: number,
mode: ProjectionMode,
) {
const visibleAxes = new Set(visibleAxesByMode[mode]);
return axisDefinitions
.filter((axis) => visibleAxes.has(axis.id))
.map((axis) => {
const start = projectPoint(axis.start, rotateX, rotateY, mode);
const end = projectPoint(axis.end, rotateX, rotateY, mode);
const vectorX = end.x - start.x;
const vectorY = end.y - start.y;
const length = Math.max(1, Math.hypot(vectorX, vectorY));
const labelX = clamp(end.x + (vectorX / length) * 34, 44, 516);
const labelY = clamp(end.y + (vectorY / length) * 34, 32, 528);
return {
...axis,
start,
end,
labelStyle: {
"--axis-label-x": `${labelX}px`,
"--axis-label-y": `${labelY}px`,
"--axis-color": axis.color,
} as CSSProperties,
};
});
}
export function ToolPositionMap() {
const [projectionMode, setProjectionMode] = useState<ProjectionMode>("xy");
const [rotateX, setRotateX] = useState(0);
const [rotateY, setRotateY] = useState(0);
const [selectedId, setSelectedId] = useState("zk");
const [hoveredId, setHoveredId] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const dragRef = useRef<{
x: number;
y: number;
rotateX: number;
rotateY: number;
} | null>(null);
const selected = useMemo(
() => tools.find((tool) => tool.id === selectedId) ?? tools[0],
[selectedId],
);
const projectedTools = useMemo(
() =>
tools
.map((tool) => ({
tool,
projected: projectPoint(
getPointCoordinates(tool),
rotateX,
rotateY,
projectionMode,
),
}))
.sort((a, b) => a.projected.z - b.projected.z),
[rotateX, rotateY, projectionMode],
);
const projectedAxes = useMemo(
() => getProjectedAxes(rotateX, rotateY, projectionMode),
[rotateX, rotateY, projectionMode],
);
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
setProjectionMode("free");
dragRef.current = {
x: event.clientX,
y: event.clientY,
rotateX,
rotateY,
};
setIsDragging(true);
event.currentTarget.setPointerCapture(event.pointerId);
};
const handlePointerMove = (event: PointerEvent<HTMLDivElement>) => {
if (!dragRef.current) return;
const deltaX = event.clientX - dragRef.current.x;
const deltaY = event.clientY - dragRef.current.y;
setRotateX(dragRef.current.rotateX - deltaY * 0.45);
setRotateY(dragRef.current.rotateY + deltaX * 0.45);
};
const handlePointerEnd = (event: PointerEvent<HTMLDivElement>) => {
dragRef.current = null;
setIsDragging(false);
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
};
return (
<section className="project-doc-section project-doc-position" id="position">
<div className="position-framing">
<div className="position-framing-copy">
<span></span>
<h3 className="position-formula">
<span></span>
<b></b>
<span></span>
<b>×</b>
<span></span>
<b>×</b>
<span></span>
</h3>
<p>
AI
</p>
</div>
<div className="position-dimension-grid">
{dimensionCards.map((card) => (
<div key={card.title}>
<strong>{card.title}</strong>
<p>{card.text}</p>
</div>
))}
</div>
<div className="position-friction-cloud">
<strong></strong>
{frictionCases.map((item) => (
<span className={`is-weight-${item.weight}`} key={item.text}>
{item.text}
</span>
))}
<em></em>
</div>
</div>
<div className="position-map-layout">
<div className="position-stage-card">
<div className="position-controls">
<div>
<span>
{projectionMode === "free"
? "拖动坐标系自由旋转"
: "二维投影视图"}
</span>
<strong>
{projectionMode === "free"
? `X ${Math.round(rotateX)} / Y ${Math.round(rotateY)}`
: presets.find((preset) => preset.mode === projectionMode)
?.name}
</strong>
</div>
<div className="position-preset-row">
{presets.map((preset) => (
<button
className={
projectionMode === preset.mode ? "is-selected" : undefined
}
key={preset.name}
onClick={() => {
setProjectionMode(preset.mode);
setRotateX(preset.rotateX);
setRotateY(preset.rotateY);
}}
type="button"
>
{preset.name}
</button>
))}
</div>
</div>
<div
className={
isDragging ? "position-stage is-dragging" : "position-stage"
}
onPointerCancel={handlePointerEnd}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerEnd}
>
<div className="position-scene">
<div className="position-space-grid" />
<svg
aria-hidden="true"
className="position-axis-overlay"
viewBox="0 0 560 560"
>
{projectedAxes.map((axis) => (
<line
key={axis.id}
stroke={axis.color}
x1={axis.start.x}
x2={axis.end.x}
y1={axis.start.y}
y2={axis.end.y}
/>
))}
</svg>
<div className="position-axis-label-layer">
{projectedAxes.map((axis) => (
<span
className="position-axis-label"
key={axis.id}
style={axis.labelStyle}
>
{axis.name}
</span>
))}
</div>
<div className="position-tool-layer">
{projectedTools.map(({ tool, projected }) => (
<button
className={[
"position-tool-anchor",
tool.id === selectedId ? "is-selected" : "",
tool.id === hoveredId ? "is-hovered" : "",
]
.filter(Boolean)
.join(" ")}
key={tool.id}
onClick={() => setSelectedId(tool.id)}
onBlur={() => setHoveredId(null)}
onFocus={() => setHoveredId(tool.id)}
onMouseEnter={() => setHoveredId(tool.id)}
onMouseLeave={() => setHoveredId(null)}
onPointerDown={(event) => event.stopPropagation()}
style={toolAnchorStyle(tool, projected)}
type="button"
>
<span className="position-ball" />
<span className="position-tool-label">{tool.name}</span>
</button>
))}
</div>
</div>
</div>
</div>
<aside className="position-detail-card">
<div className="position-score">
<span>{selected.type}</span>
<strong>{score(selected)}</strong>
</div>
<h3>{selected.name}</h3>
<p>{selected.summary}</p>
<div className="position-meter-list">
<div>
<span></span>
<i style={{ width: `${selected.x}%` }} />
</div>
<div>
<span></span>
<i style={{ width: `${selected.y}%` }} />
</div>
<div>
<span></span>
<i style={{ width: `${selected.z}%` }} />
</div>
</div>
<dl>
<div>
<dt></dt>
<dd>{selected.fit}</dd>
</div>
<div>
<dt></dt>
<dd>{selected.limit}</dd>
</div>
</dl>
</aside>
</div>
</section>
);
}
+1025 -1629
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,197 @@
# 01. 基座 Runtime 架构
![基座 Runtime 架构](assets/01-base-runtime.png)
## 1. 基座解决的问题
基座不绑定某一个业务流程。它提供的是通用 Agent runtime
- 多用户 Web 入口。
- 多会话状态管理。
- 模型选择和 OpenAI-compatible 调用。
- Tool registry 和 tool handler 执行。
- Skill 发现、启用和提示词注入。
- 当前会话工作区。
- 运行态、事件流、活动区和刷新恢复。
- 用户记忆和 Skill 记忆后台。
业务能力例如 `product-data``online-mining-v2``label-master` 都跑在这个基座上。
## 2. 文字架构图
```text
浏览器 / Web UI
|
| 用户消息、模型选择、Skill 启用、文件面板、停止运行
v
frontend/app
|
| /api/chat、/api/claw/*、/admin
v
backend/api/server.py
|
| 账号配置、会话目录、run manager、run_state_store、memory_manager
v
LocalCodingAgent
|
| build_session、prompt sections、tool_specs、Agent loop
v
OpenAICompatClient
|
| messages + tools -> model backend
v
模型
|
| assistant text 或 tool_calls
v
Tool Runtime
|
| read/write/python_exec/data_agent/MCP/search/bash
v
session workspace
|
| input / scratchpad / output / session.json
v
前端活动区和文件面板
运行结束后:
AgentRunResult
-> session_store 持久化
-> memory_manager.enqueue_interaction
-> memory worker 异步整理
-> 下一轮 render_injection 注入
```
## 3. 关键代码入口
### 3.1 Web 后端入口
主要文件:
```text
backend/api/server.py
```
关键职责:
- 维护账号和会话配置。
- 创建或复用 `LocalCodingAgent`
- 给 Agent 注入 `runtime_context`、记忆、Jupyter 上下文。
- 记录 run event,并通过 NDJSON streaming 返回前端。
- 运行结束后写入 elapsed、标题、memory event。
关键函数和位置:
```text
backend/api/server.py:691 enabled_skill_names(...)
backend/api/server.py:822 _build_agent(...)
backend/api/server.py:853 agent_for(...)
backend/api/server.py:866 run_lock_for(...)
backend/api/server.py:1720 /api/chat 运行链路开始
backend/api/server.py:1739 memory_manager.render_injection(...)
backend/api/server.py:1751 emit_agent_event(...)
backend/api/server.py:1897 序列化运行结果和 elapsed
backend/api/server.py:1920 memory_manager.enqueue_interaction(...)
backend/api/server.py:2034 StreamingResponse NDJSON
```
### 3.2 Agent Runtime
主要文件:
```text
src/agent_runtime.py
```
关键职责:
- 初始化 tool registry、plugin runtime、MCP runtime、search runtime 等。
- 新会话用 `run(...)`,旧会话用 `resume(...)`
-`_run_prompt(...)` 中执行完整 Agent loop。
- 维护 usage、cost、tool_calls、events、file_history。
- 在结束时持久化 session。
关键函数和位置:
```text
src/agent_runtime.py:158 class LocalCodingAgent
src/agent_runtime.py:195 __post_init__
src/agent_runtime.py:413 run(...)
src/agent_runtime.py:441 resume(...)
src/agent_runtime.py:520 _run_prompt 主链路
src/agent_runtime.py:552 tool_specs = [tool.to_openai_tool() ...]
src/agent_runtime.py:619 for turn_index in range(...)
src/agent_runtime.py:1008 遍历模型返回的 tool_calls
src/agent_runtime.py:1490 _query_model(...)
```
### 3.3 系统提示词
主要文件:
```text
src/agent_prompting.py
```
关键职责:
- 定义中控 Agent 身份。
- 注入工具使用策略。
- 注入工作空间边界。
- 注入 Skill 列表。
- 注入 ask_user 等 runtime 指导。
关键函数和位置:
```text
src/agent_prompting.py:135 get_intro_section
src/agent_prompting.py:155 get_doing_tasks_section
src/agent_prompting.py:182 get_actions_section
src/agent_prompting.py:196 get_workspace_boundary_section
src/agent_prompting.py:210 get_using_your_tools_section
src/agent_prompting.py:274 get_skill_guidance_section
src/agent_prompting.py:414 get_ask_user_guidance_section
```
## 4. 基座的分层职责
```text
Web UI
负责交互、展示、文件面板、活动区、Skill 勾选、管理后台。
Backend API
负责账号、会话、模型配置、运行态、服务端事件流。
Agent Runtime
负责 Agent loop、模型调用、工具调用、预算和持久化。
Prompting
负责把规则、工具、公约、Skill 列表转成模型可见上下文。
Tool Runtime
负责稳定执行动作,并把结果结构化回传给模型。
Skill System
负责让业务流程、知识、脚本可被 Agent 发现和使用。
Workspace
负责隔离每个用户和每个会话的输入、临时文件和交付产物。
Memory Worker
负责从交互历史中异步整理长期偏好和 Skill 使用经验。
```
## 5. 设计边界
基座只应该承载跨业务复用的稳定能力,例如:
- 工具执行。
- 会话状态。
- 运行态事件。
- 工作区路径。
- Skill 发现和启用。
- 模型适配。
- 记忆后台。
业务流程不应该写死在基座里。数据生成、线上挖掘、标签判断等变化快的流程应该沉到 Skill;确定性脚本应该放在对应 Skill 的 `scripts/` 下。
@@ -0,0 +1,246 @@
# 02. Agent Loop 执行机制
![Agent Loop 执行机制](assets/02-agent-loop.png)
## 1. Agent Loop 的基本形态
Agent 每轮不是只调用一次模型,而是一个循环:
```text
用户输入
-> 构造 session 和 prompt
-> 调模型
-> 模型返回 assistant text 或 tool_calls
-> 如果没有 tool_calls:输出最终回复,结束
-> 如果有 tool_calls:执行工具
-> 工具结果写回 session
-> 下一轮模型继续读取工具结果
-> 直到最终回复、预算超限、取消、max_turns 或 review 停止
```
这个循环允许 Agent 做“观察-执行-再观察”的任务,例如:
- 先读文件,再决定是否需要抽取。
- 先生成 plan,等待用户 review。
- 先运行脚本,再根据校验结果修复。
- 先查询线上数据,再抽样展示。
## 2. 新会话和恢复会话
新会话入口:
```text
src/agent_runtime.py:413 run(...)
```
关键动作:
```text
1. 清理当前 managed_agent_id 和 resume_source_session_id。
2. 创建 session_id。
3. 创建 scratchpad_directory。
4. 绑定 plan_runtime/task_runtime 到 scratchpad。
5. 调 _run_prompt(...)
6. 累计 usage。
```
恢复会话入口:
```text
src/agent_runtime.py:441 resume(...)
```
关键动作:
```text
1. 从 StoredAgentSession 恢复 AgentSessionState。
2. 回放 file_history 和 compaction 信息。
3. 设置 active_session_id 和 last_session_path。
4. 恢复 plugin state。
5. 复用已有 scratchpad_directory。
6. 调 _run_prompt(...)
```
这解释了为什么刷新页面、回到旧会话后,Agent 理论上可以继续同一个 session 的上下文,而不是新建一个任务。
## 3. `_run_prompt` 主链路
核心位置:
```text
src/agent_runtime.py:520 _run_prompt 主体
```
主链路关键步骤:
```text
1. slash command 预处理。
2. hook policy / plugin hook 修改 prompt。
3. agent_manager.start_agent(...) 记录运行。
4. 新建或复用 AgentSessionState。
5. runtime_context prepend 到模型可见的用户消息。
6. session.append_user(...)。
7. tool_context 注入 scratchpad、plan_runtime、task_runtime。
8. 生成 tool_specs。
9. 初始化 usage、cost、tool_calls、events。
10. 进入 turn loop。
```
对应代码点:
```text
src/agent_runtime.py:523 agent_manager.start_agent(...)
src/agent_runtime.py:531 session = base_session or build_session(...)
src/agent_runtime.py:539 _prepend_runtime_context(...)
src/agent_runtime.py:543 session.append_user(...)
src/agent_runtime.py:546 replace(self.tool_context, scratchpad_directory=...)
src/agent_runtime.py:552 tool_specs = [tool.to_openai_tool() ...]
src/agent_runtime.py:582 stream_events = _RuntimeEventBuffer(event_sink)
src/agent_runtime.py:619 for turn_index in range(...)
```
## 4. 模型调用和 tool_calls 判断
模型调用入口:
```text
src/agent_runtime.py:1490 _query_model(...)
```
非流式路径中:
```text
turn = self.client.complete(
session.to_openai_messages(),
tool_specs,
output_schema=...
)
```
模型能否返回 `tool_calls` 取决于:
- 当前 `messages`
- 系统提示词。
- Skill 列表。
- 工具描述和参数 schema。
- 模型自身 tool calling 能力。
基座不会强制某个工具被调用。基座只把工具能力暴露给模型,并在模型返回 `tool_calls` 后负责执行。
## 5. 没有 tool_calls 时
如果模型本轮没有工具调用:
```text
src/agent_runtime.py:763 if not turn.tool_calls
```
后续可能有三种情况:
1. 直接输出最终回复。
2. 如果模型输出被截断,自动追加 continuation prompt。
3. 如果达到 continuation 限制,则追加提示并结束。
最终会:
```text
session.append_assistant(...)
_append_final_text_stream_events(...)
AgentRunResult(...)
_persist_session(...)
```
## 6. 有 tool_calls 时
如果模型返回工具调用:
```text
src/agent_runtime.py:1008 for tool_call in turn.tool_calls
```
每个工具调用会:
```text
1. tool_calls 计数 +1。
2. 检查预算。
3. session.start_tool(...) 写入工具开始消息。
4. stream_events 追加 tool_start。
5. 根据工具名执行 handler。
6. 工具结果写回 session。
7. stream_events 追加工具结果。
8. 下一轮模型读取工具结果继续判断。
```
关键代码点:
```text
src/agent_runtime.py:1054 session.start_tool(...)
src/agent_runtime.py:1060 stream_events.append(type='tool_start')
src/agent_tools.py:60 execute_tool(...)
src/agent_tools.py:76 execute_tool_streaming(...)
```
## 7. 为什么 review 可以暂停流程
review 门禁不是特殊 UI 魔法,而是 Agent loop 的自然结果:
1. Skill 要求模型在某一步调用 review 工具。
2. 工具创建 pending state,并返回需要展示的信息。
3. Agent 生成回复,告诉用户需要确认。
4. 当前 run 结束。
5. 用户下一轮回复“确认”。
6. Agent resume 旧 session,调用 confirm 工具。
7. 后续流程继续。
例如 `product-data`
```text
data_agent_prepare_generation_goal
-> pending goal
-> 停止等待用户确认
data_agent_confirm_generation_goal
-> confirmed_goal_id
-> data_agent_prepare_generation_plan
-> pending plan
-> 停止等待用户确认
data_agent_confirm_generation_plan
-> confirmed_plan_id
-> 允许生成 draft 和转换 records
```
## 8. 预算、取消和 max_turns
基座会在模型调用前后、工具请求前检查预算:
```text
src/agent_runtime.py:592 initial_budget = self._check_budget(...)
src/agent_runtime.py:732 budget_after_model = self._check_budget(...)
src/agent_runtime.py:1014 budget_after_tool_request = self._check_budget(...)
```
如果达到最大轮次:
```text
src/agent_runtime.py:1440 _build_max_turns_output(...)
```
输出会包含最后一次阶段说明,提醒用户可以继续补充指令。
取消由后端 run manager 和 tool process registry 处理。Web 点停止后,后端将 run 标记取消,并让工具执行上下文感知 cancel_event。
## 9. 事件流和前端活动区
Agent loop 内会不断追加 `stream_events`,后端用 `event_sink` 把事件推给前端。
后端关键代码:
```text
backend/api/server.py:1751 emit_agent_event(event)
backend/api/server.py:1756 state.run_manager.record_event(...)
backend/api/server.py:1758 state.run_state_store.record_event(...)
backend/api/server.py:2034 StreamingResponse(..., media_type='application/x-ndjson')
```
前端活动区展示的阶段说明、工具开始、工具完成、最终文本,本质上都来自这些 runtime events 或 session replay。
@@ -0,0 +1,298 @@
# 03. 工具体系和 Tool Handler
![工具体系和 Tool Handler](assets/03-tools.png)
## 1. 工具体系的职责
Tool 是执行层。模型只能“请求调用工具”,不能直接执行工具。
后端根据工具名找到 handler,校验参数,执行动作,并把结果写回 session。
工具适合承载:
- 稳定文件读写。
- Python 执行。
- Python 包安装。
- 数据格式转换和校验。
- review 状态机。
- 外部系统访问。
- 需要权限、取消、路径约束或审计的动作。
## 2. Tool registry 构成
入口:
```text
src/agent_tools.py:105 default_tool_registry()
```
它会合并多类工具:
```text
build_file_tools(...)
build_execution_tools(...)
LSP / web_fetch / search / account / config / task / team / workflow 等
Skill 工具
data_agent 工具
```
关键片段:
```text
src/agent_tools.py:107 build_file_tools
src/agent_tools.py:116 build_execution_tools
src/agent_tools.py:1012 AgentTool(name='Skill')
src/agent_tools.py:1033 build_data_agent_tools
```
返回结果是:
```python
return {tool.name: tool for tool in tools}
```
## 3. Tool spec 注入模型
在 Agent loop 中:
```text
src/agent_runtime.py:552
tool_specs = [tool.to_openai_tool() for tool in self.tool_registry.values()]
```
每个工具包含:
```text
name
description
parameters JSON schema
handler
```
模型看到的是 name、description、parameters。handler 不暴露给模型,只在后端执行。
## 4. Tool handler 执行路径
工具执行入口:
```text
src/agent_tools.py:60 execute_tool(...)
src/agent_tools.py:76 execute_tool_streaming(...)
```
执行逻辑:
```text
1. 从 tool_registry 取 tool。
2. 找不到则返回 Unknown tool。
3. bash 走 streaming。
4. 其他工具调用 tool.execute(arguments, context)。
5. 工具结果序列化回模型和前端。
```
## 5. 文件工具
定义位置:
```text
src/agent_tool_specs/files.py
```
主要工具:
- `list_dir`
- `read_file`
- `write_file`
- `edit_file`
- `notebook_edit`
- `glob_search`
- `grep_search`
### 5.1 `write_file` 的边界
`write_file` 当前明确定位为“小文件工具”:
```text
src/agent_tool_specs/files.py
Write or append a SMALL UTF-8 file inside the workspace.
```
适合:
- 短 Markdown。
- 少量配置。
- 小 JSON。
- 少量 JSONL。
- 小 CSV。
不适合:
- 长 Python 脚本。
- 大 JSON。
- JSONL 数据集。
- 长 CSV。
- 大段包含引号和换行的内容。
原因是模型生成工具参数时,需要把内容嵌入 JSON 参数。长文本、引号、换行会显著增加非法 JSON 参数概率。
因此系统提示词也要求:
```text
复杂写文件优先用 python_exec 通过 pathlib/json/csv 写入。
```
## 6. Python 执行工具
定义位置:
```text
src/agent_tool_specs/execution.py
```
主要工具:
- `python_exec`
- `python_package`
- `bash`
- `sleep`
### 6.1 `python_exec`
定位:
```text
结构化文件分析、JSON/JSONL 处理、批量校验、数据抽样、快速计算。
```
支持两种模式:
```text
code
一次性 Python 代码。
script_path
调用项目或 Skill 内已有脚本。
```
重要约束:
- 不用 `bash python ...`
- 不在 `python_exec.code` 里用 subprocess 二次调用 Python 脚本。
- 临时文件写入 `PYTHON_EXEC_SCRATCHPAD`
- 交付产物写入 session/output。
### 6.2 `python_package`
定位:
```text
给当前用户独立 Python venv 安装缺失包。
```
典型场景:
- `pandas`
- `pyarrow`
- `openpyxl`
- `elasticsearch`
- `urllib3`
设计上避免安装到项目 `.venv` 或系统 Python。
### 6.3 `bash`
`bash` 是兜底工具,不是默认 Python 执行方式。
适合:
- git 只读检查。
- 系统命令。
- 进程控制。
- 用户确认后的依赖安装或服务管理。
不适合:
- Python 数据分析。
- JSON/CSV 转换。
- 包安装。
## 7. 路径解析和 session 路由
关键实现:
```text
src/agent_tools.py:1589 _data_agent_output_path(...)
src/agent_tools.py:1624 _data_agent_session_output_root(...)
src/agent_tools.py:1630 _resolve_path(...)
src/agent_tools.py:1658 _session_logical_path(...)
src/agent_tools.py:1685 _execution_cwd(...)
```
逻辑路径会被映射到当前 session:
```text
output/... -> session/output/...
outputs/... -> session/output/...
scratchpad/... -> session/scratchpad/...
scratch/... -> session/scratchpad/...
input/... -> session/input/...
inputs/... -> session/input/...
```
`python_exec` 的执行 cwd 优先是当前会话 scratchpad
```text
src/agent_tools.py:1685 _execution_cwd(...)
```
## 8. data_agent 工具
声明位置:
```text
src/agent_tool_specs/data_agent.py
```
handler 注册位置:
```text
src/agent_tools.py:1033 build_data_agent_tools(...)
```
主要工具分两类:
### 8.1 前链路和 review 状态机
- `data_agent_load_input_sources`
- `data_agent_render_source_context`
- `data_agent_extract_case_evidence`
- `data_agent_prepare_generation_goal`
- `data_agent_confirm_generation_goal`
- `data_agent_prepare_generation_plan`
- `data_agent_show_generation_plan`
- `data_agent_update_generation_plan`
- `data_agent_confirm_generation_plan`
这类工具当前仍属于平台工具,因为它们维护 pending/confirmed 状态,以及产品数据生成的 review 门禁。
### 8.2 历史格式转换工具
- `data_agent_normalize_dataset_draft`
- `data_agent_validate_dataset_records`
- `data_agent_export_dataset_records`
- `data_agent_export_training_jsonl`
- `data_agent_export_planning_eval_csv`
这部分能力已经逐步迁移到 `skills/product-data/scripts/`,平台工具更多是历史兼容和适配层。
## 9. 工具设计原则
当前工具体系的技术取舍:
```text
模型负责决策。
工具负责执行。
Skill 负责流程经验。
脚本负责可迁移确定性能力。
```
工具描述要足够明确,否则模型会选错工具;参数 schema 要尽量简单,否则不同模型后端可能不兼容。
@@ -0,0 +1,256 @@
# 04. Skill 体系和能力包约定
![Skill 体系和能力包约定](assets/04-skills.png)
## 1. Skill 的定位
Skill 是经验层。它不是单纯 prompt,也不是单纯脚本。
一个 Skill 应该回答:
- 什么场景触发。
- 输入材料是什么。
- Agent 应该按什么流程做。
- 哪些地方必须让用户 review。
- 应该调用哪些工具或脚本。
- 产物应该写到哪里。
- 哪些做法是禁止的。
稳定可执行逻辑不应该长期写在 Skill 文本里,而应该进入:
```text
skills/<skill-name>/scripts/
```
或者平台级工具。
## 2. Skill loader 实现
关键文件:
```text
src/bundled_skills.py
```
项目级 Skill 目录:
```text
skills/<skill-name>/SKILL.md
```
核心数据结构:
```text
src/bundled_skills.py:28 BundledSkill
```
字段:
```text
name
description
when_to_use
aliases
allowed_tools
user_invocable
source
path
get_prompt
```
## 3. SKILL.md 解析
解析逻辑:
```text
src/bundled_skills.py:147 _parse_front_matter
src/bundled_skills.py:176 _load_directory_skill
```
`SKILL.md` 必须有 frontmatter
```yaml
---
name: product-data
description: 从产品/标签定义、手写边界规则或示例 query 中提取标签边界...
when_to_use: 当用户提供产品定义、标签规则...
aliases: definition-data, label-data
allowed_tools: read_file, write_file, python_exec
---
```
解析后:
- frontmatter 进入 `BundledSkill` 元数据。
- body 作为真正的 Skill prompt。
- 如果调用 Skill 时带 args,会追加到 `## Invocation Arguments`
对应实现:
```text
src/bundled_skills.py:138 _directory_skill_prompt
```
## 4. Skill 发现顺序
项目 Skill 发现入口:
```text
src/bundled_skills.py:199 load_directory_skills
src/bundled_skills.py:215 load_project_skills
```
系统提示词中可见 Skill 列表由:
```text
src/bundled_skills.py:270 format_skills_for_system_prompt
```
生成。
Web 后端会按当前账号和 session 配置计算启用 Skill
```text
backend/api/server.py:691 enabled_skill_names
backend/api/server.py:706 set_skill_enabled
backend/api/server.py:738 set_all_skills_enabled
```
这意味着:
- Skill 可以存在于项目中,但不一定对某个 session 启用。
- 启用状态影响系统提示词里的 Skill 列表。
- 被禁用的 Skill 不应该被模型主动选择。
## 5. Skill 工具
Skill 本身也是一个工具:
```text
src/agent_tools.py:1012 AgentTool(name='Skill')
```
模型调用:
```json
{
"skill": "product-data",
"args": "用户原始需求或显式参数"
}
```
执行后,Skill body 会被加入对话,让模型按 Skill 中的流程继续做任务。
## 6. Skill 和 Agent Loop 的关系
Skill 不会替代 Agent loop,而是改变 Agent loop 的下一步决策依据。
典型模式:
```text
用户提出任务
-> 模型从 Skill 列表中选择某个 Skill
-> 调用 Skill 工具
-> Skill.md 正文进入上下文
-> 模型按 Skill 指令调用 read_file/python_exec/data_agent 等工具
-> 工具结果进入上下文
-> 模型继续按 Skill 流程推进
```
因此 Skill 的好坏直接影响:
- 模型能否召回正确能力。
- 是否会过早执行。
- 是否能在 review 门禁停下来。
- 是否能使用正确工具而不是手写不稳定逻辑。
## 7. 推荐 Skill 目录结构
```text
skills/<skill-name>/
SKILL.md
README.md
knowledge/
scripts/
examples/
schemas/
tools.yaml
requirements.txt
```
各部分职责:
```text
SKILL.md
运行时入口,写流程、门禁、工具调用方式和禁止事项。
README.md
给维护者看的说明,不一定进入模型上下文。
knowledge/
业务知识、标签规则、字段说明、边界案例。
scripts/
确定性脚本,优先通过 python_exec.script_path 执行。
examples/
示例输入输出,用于回归和讲解。
schemas/
JSON schema 或字段约定。
tools.yaml
描述 portable scripts 如何注册为工具,便于迁移到其他 Agent。
requirements.txt
Skill 脚本的 Python 依赖。
```
## 8. Skill 更新
Web 后端提供 Skill 更新能力:
```text
backend/api/server.py:765 sync_skills_from_git
```
核心行为:
```text
1. 检查当前目录是否是 git 仓库。
2. 检查 tracked 文件是否干净。
3. git fetch origin。
4. git pull --ff-only origin 当前分支。
5. 清理当前账号 agent cache。
6. 重新读取 get_bundled_skills。
```
这让新增或修改 Skill 后,不一定需要重启服务才能让 Skill 列表刷新。
## 9. Skill 设计边界
适合写在 Skill
- 工作流。
- 何时提问。
- 何时 review。
- 哪些工具优先。
- 输出位置约定。
- 常见失败经验。
不适合长期写在 Skill
- 大段可检索知识。
- 复杂代码。
- 格式转换。
- 查询外部系统的具体实现。
- 需要校验的稳定数据结构。
这些应该分别放到:
```text
knowledge/
scripts/
schemas/
platform tools
```
@@ -0,0 +1,243 @@
# 05. 会话工作区、运行态和记忆
![会话工作区、运行态和记忆](assets/05-workspace-memory-observability.png)
## 1. 会话工作区
每个用户、每个会话都有独立目录:
```text
.port_sessions/accounts/<account_id>/sessions/<session_id>/
input/
scratchpad/
output/
session.json
```
目录职责:
```text
input/
用户上传或明确提供的输入资料。
scratchpad/
临时脚本、中间文件、抽样缓存、断点记录。
output/
最终交付产物。
session.json
会话消息、工具调用、usage、events、file_history、runtime metadata。
```
## 2. 工作区路径路由
路径解析实现:
```text
src/agent_tools.py:1658 _session_logical_path
```
逻辑路径:
```text
output/... -> session/output/...
outputs/... -> session/output/...
scratchpad/... -> session/scratchpad/...
scratch/... -> session/scratchpad/...
input/... -> session/input/...
inputs/... -> session/input/...
```
Python 执行 cwd
```text
src/agent_tools.py:1685 _execution_cwd
```
优先使用当前 session scratchpad。这是为了避免临时脚本和缓存污染项目根目录。
## 3. 平台代码写保护
相关实现:
```text
src/agent_tools.py:1118 _PLATFORM_READONLY_DIRS
src/agent_tools.py:1698 _is_platform_code_path
src/agent_tools.py:1708 _ensure_not_platform_code_write
```
当前平台目录:
```text
src
backend
frontend
scripts
```
在数据 Agent 会话中默认视为只读。除非用户明确进入平台开发任务,否则业务任务不应修改平台代码。
## 4. Run 状态和活动区
后端在 `/api/chat` 运行时创建 run record
```text
backend/api/server.py:1720 run_record = state.run_manager.start(...)
backend/api/server.py:1722 state.run_state_store.start(...)
```
运行事件通过 `emit_agent_event` 记录:
```text
backend/api/server.py:1751 emit_agent_event
backend/api/server.py:1756 run_manager.record_event
backend/api/server.py:1758 run_state_store.record_event
```
返回前端:
```text
backend/api/server.py:2034 StreamingResponse
```
前端活动区看到的内容主要来自:
- `run_started`
- `tool_start`
- `tool_result`
- 模型阶段说明
- final text stream events
- run finish/error/cancel 状态
## 5. 会话持久化
运行结束后,后端序列化结果:
```text
backend/api/server.py:2056 _serialize_run_result
backend/api/server.py:2069 _normalize_transcript_entry
```
会话读取:
```text
backend/api/server.py:2094 _serialize_stored_session
```
`agent_runtime` 在多个结束路径都会调用:
```text
_persist_session(session, result)
```
这让刷新后可以恢复:
- 用户消息。
- assistant 文本。
- tool_calls。
- tool result。
- elapsed。
- file_history。
## 6. 记忆体系
实现位置:
```text
src/personal_memory.py
```
### 6.1 文件和数据库
```text
memory.db
user.md
skills/<skill-name>.md
```
常量:
```text
src/personal_memory.py:26 MEMORY_DB_FILENAME
src/personal_memory.py:27 USER_MEMORY_FILENAME
src/personal_memory.py:28 SKILL_MEMORY_DIRNAME
```
### 6.2 注入逻辑
```text
src/personal_memory.py:102 render_injection
```
注入规则:
```text
1. 读取 user.md。
2. 根据 enabled_skill_names 读取对应 skills/<skill>.md。
3. 拼成 # 个性化记忆。
4. 如果用户本轮要求冲突,以本轮要求为准。
```
后端调用:
```text
backend/api/server.py:1739 memory_manager.render_injection(...)
```
### 6.3 入队逻辑
运行结束后:
```text
backend/api/server.py:1920 memory_manager.enqueue_interaction(...)
```
记忆模块内:
```text
src/personal_memory.py:138 enqueue_interaction
src/personal_memory.py:635 detect_memory_signals
```
会检测:
- 显式记忆词:记住、以后、下次、默认、总是、不要、应该、固定。
- 纠错词:不对、不是这样、格式错、之前说过、还是不行。
- Skill/工具/流程/格式相关表述。
- 工具参数非法 JSON 等工具经验。
### 6.4 后台整理
核心逻辑:
```text
src/personal_memory.py:378 _consolidate_events
src/personal_memory.py:419 _generate_memory_updates
src/personal_memory.py:471 _fallback_memory_updates
```
设计取舍:
- 主链路不直接生成记忆。
- 事件先进入 SQLite 队列。
- 后台 worker 批量整理。
- LLM 失败时有 fallback 规则。
- Markdown 是最终可编辑记忆正文。
## 7. 可观测性设计
当前可观测性来自三个层次:
```text
运行态
run_manager + run_state_store,支持运行中刷新、停止、恢复活动区。
会话态
session.json,保存完整消息和工具调用。
产物态
session/input、scratchpad、output,文件面板可查看和下载。
```
这个设计让用户不仅看到最终回复,也能看到 Agent 做了什么、文件在哪里、失败在哪个工具或阶段。
@@ -0,0 +1,292 @@
# 06. product-data Skill 实现
![product-data Skill 实现](assets/06-product-data.png)
## 1. 定位
`product-data` 是数据开发链路的核心 Skill,负责:
```text
产品定义 / 标签规则 / 手写边界 / 示例 query / badcase
-> 输入文本化
-> generation goal
-> 用户 review
-> generation plan
-> 用户确认数量、标签、边界、路径
-> dataset draft text
-> canonical records
-> validate
-> export records / table / training / eval
```
位置:
```text
skills/product-data/SKILL.md
skills/product-data/knowledge/
skills/product-data/scripts/
```
## 2. Skill 目录结构
当前 `SKILL.md` 中定义的能力组织:
```text
skills/product-data/
SKILL.md
tools.yaml
requirements.txt
knowledge/
dataset_draft_v1.md
canonical_record_v1.md
portable_skill_contract.md
schemas/
scripts/
normalize_dataset_draft.py
validate_dataset_records.py
export_dataset_records.py
export_dataset_table.py
export_training_jsonl.py
export_planning_eval_csv.py
```
分工:
```text
SKILL.md
流程协议、门禁、工具调用顺序、禁止事项。
knowledge/
数据草稿、canonical record 和 portable skill contract。
scripts/
确定性转换、校验和导出。
data_agent_* 平台工具
负责输入文本化和 review 状态机。
```
## 3. 输入类型
Skill 将输入分成三类:
```text
文件定义型
产品定义、标签定义、路由规则、表格、Markdown、CSV。
手写规则型
用户直接描述边界,例如“找附近美食给餐饮服务,导航去某地给地图导航”。
示例归纳型
用户只给 query/example/badcase,需要先归纳边界和标签倾向。
```
三类输入最后统一整理成:
```text
dataset_label
target / target_definitions
plan_hint
coverage
exclusions
open_questions
source_refs
complex 规则
```
这一步称为 `generation_goal`
## 4. Review 门禁
`product-data` 有两个门禁模式。
### 4.1 一次确认模式
适用:
- 用户直接给出清晰手写规则。
- target 表达明确。
- 用户已经希望生成数据。
链路:
```text
data_agent_prepare_generation_plan(direct_review=true)
-> 展示目标 + 数量 + 路径
-> 等待“确认,开始生成”
```
### 4.2 两段确认模式
适用:
- 用户提供文件、表格、badcase、长文档。
- 标签、边界、字段有歧义。
- 需要先从资料中抽取 generation goal。
链路:
```text
data_agent_load_input_sources
-> data_agent_render_source_context
-> Agent 整理 generation_goal
-> data_agent_prepare_generation_goal
-> 用户确认目标
-> data_agent_confirm_generation_goal
-> data_agent_prepare_generation_plan
-> 用户确认计划
-> data_agent_confirm_generation_plan
```
## 5. 和 Agent Loop 的关系
`product-data` 明确利用 Agent loop 做分阶段控制。
```text
第一轮:
模型选择 product-data
调输入工具
调 prepare_generation_goal 或 prepare_generation_plan
输出 review 信息
停止
第二轮:
用户确认目标或计划
Agent resume session
调 confirm 工具
继续下一阶段
第三轮:
用户确认计划
Agent 生成 dataset draft
每批调用 normalize 脚本
调 validate 脚本
调 export 脚本
输出最终文件路径
```
重点是:确认状态不是靠自由文本记忆,而是由工具维护 `confirmed_goal_id``confirmed_plan_id`
## 6. 数据格式设计
### 6.1 dataset draft text
面向模型生成,要求模型用较低结构负担描述:
- 用户 / 小爱 对话。
- 当前 query。
- target。
- complex。
- 场景说明。
设计目标是降低模型直接写 JSONL 的难度。
### 6.2 canonical record
面向工具处理,字段稳定。
用于:
- 校验结构。
- 导出流转 CSV。
- 导出训练 JSONL。
- 导出评测 planningPrompt CSV。
### 6.3 complex 独立维度
`complex` 不属于 target。
内部保存:
```text
complex: false
target: Agent(tag="地图导航")
```
训练输出组合:
```text
complex=false
Agent(tag="地图导航")
```
评测 CSV 里:
```text
code标签: Agent(tag="地图导航")
complex: FALSE
```
## 7. 脚本链路
生成式数据确认后,Skill 要求使用 `python_exec.script_path` 调脚本。
典型顺序:
```text
normalize_dataset_draft.py
输入 draft + confirmed_plan_id
输出/追加 scratchpad/normalized_records.jsonl
validate_dataset_records.py
读取 normalized_records.jsonl
校验字段、target、complex、时间戳、多轮上下文
export_dataset_records.py
导出 output/records.jsonl
同时生成 output/records.csv
export_training_jsonl.py
导出 output/training.jsonl
export_planning_eval_csv.py
导出 output/eval_planning.csv
```
为什么不用 `write_file`
- records、CSV、JSONL 都是强格式数据。
- 模型手写容易出错。
- 脚本能统一 timestamp、prev_session、context、complex/target 组合。
## 8. 输出约束
固定逻辑输出:
```text
output/records.jsonl
output/records.csv
output/training.jsonl
output/eval_planning.csv
```
通过路径路由,实际写入:
```text
.port_sessions/accounts/<account>/sessions/<session>/output/
```
Skill 明确禁止:
- 按数据集名创建随机子目录。
- 把 records 写到项目根目录。
-`write_file` 手写最终 JSONL/CSV。
- 在未确认 plan 前生成数据。
## 9. 设计边界
`product-data` 负责:
- 数据目标对齐。
- 生成计划 review。
- dataset draft 生成协议。
- canonical records 转换和导出。
不负责:
- 判断所有标签知识。
- 查询线上数据。
- ELK 检索。
- 模型训练或 git 数据仓库提交。
标签知识应由 `label-master` 辅助,线上数据由 `online-mining-v2` 或相关 Skill 获取。
@@ -0,0 +1,177 @@
# 07. online-mining-v2 Skill 实现
![online-mining-v2 Skill 实现](assets/07-online-mining-v2.png)
## 1. 定位
`online-mining-v2` 用于从线上 ELK 日志中挖掘 case,并把候选样本转换成 `product-data` 兼容的标准数据。
典型链路:
```text
线上挖掘需求
-> 明确目标标签、complex、筛选特征
-> 探索 ELK 表字段
-> 搜索候选
-> 抽样 review
-> 调整策略
-> 直接转 canonical records
-> 或转 product-data 生成补数
```
位置:
```text
skills/online-mining-v2/SKILL.md
skills/online-mining-v2/knowledge/
skills/online-mining-v2/scripts/
```
## 2. 能力组织
`online-mining-v2` 不新增平台注册工具。它把能力放在 Skill 目录下,通过 `python_exec.script_path` 执行。
```text
scripts/
online_mining_common.py
elk_profile_index.py
elk_search_cases.py
elk_fetch_by_request_ids.py
elk_join_request_logs.py
build_dataset_draft.py
build_online_records.py
```
这样做的原因:
- ELK 查询逻辑属于该 Skill 的业务能力。
- 迁移到其他 Agent 时可以直接跑脚本。
- 平台只需要提供通用 `python_exec``python_package`
## 3. 默认数据源
当前重点支持两类索引:
```text
pre-processing*
前处理日志。
适合拿模型 prompt、模型输出、候选 domain、excellent_domains_result。
arch-flat-nlp-log-f-*
主 NLP 日志。
适合拿 query、domain、func、request_id、device_id、device、tts/text/to_speak。
```
Skill 文档中会引导 Agent
- 先用 `elk_profile_index.py` 看字段和样本。
- 再用 `elk_search_cases.py` 根据 query/domain/model output 搜索。
- 必要时用 request_id 做双表 join。
## 4. 和 Agent Loop 的关系
这个 Skill 的交互不是“一次查询结束”,而是策略迭代:
```text
用户描述需求
-> Agent 整理目标标签和筛选特征
-> 如缺目标标签或字段,先问用户
-> python_exec 调 elk_profile_index.py
-> 模型观察字段和样本
-> python_exec 调 elk_search_cases.py
-> 模型观察候选质量
-> 抽样展示给用户 review
-> 用户说哪里不对
-> 调整 filters / regex / domain / model output 条件
-> 再搜索
```
Agent loop 的价值在这里很明显:每次工具结果都会进入上下文,模型可以基于真实候选调整策略。
## 5. 两条分支
`online-mining-v2` 最重要的设计是强制区分两个分支。
### 5.1 直接线上样本分支
适用:
```text
用户想把线上筛出来的真实 case 作为评测集或专项集。
```
行为:
```text
不生成新 query。
不进入 product-data generation plan。
用 build_online_records.py 直接转 canonical records。
```
典型输出:
```text
output/records.jsonl
output/records.csv
```
### 5.2 补充生成分支
适用:
```text
用户想围绕线上问题扩写更多类似 case。
```
行为:
```text
先分析线上 badcase。
整理错误类型和覆盖目标。
再转 product-data 的 generation goal / plan / draft / records 流程。
```
这条边界避免了旧流程里出现的错误:用户只是想“把候选转样本”,Agent 却误走“生成新数据”。
## 6. 和 product-data 的关系
`online-mining-v2` 后处理复用 `product-data` 的标准:
```text
canonical record v1
records.jsonl
records.csv
training.jsonl
eval_planning.csv
```
复用脚本:
```text
skills/product-data/scripts/normalize_dataset_draft.py
skills/product-data/scripts/validate_dataset_records.py
skills/product-data/scripts/export_dataset_records.py
skills/product-data/scripts/export_training_jsonl.py
skills/product-data/scripts/export_planning_eval_csv.py
```
因此线上挖掘和产品定义生成最终可以进入同一套数据格式。
## 7. 设计边界
`online-mining-v2` 负责:
- ELK 字段探索。
- ELK 条件搜索。
- request_id 补全。
- 候选样本 review。
- 线上候选转 canonical records。
不负责:
- 定义 canonical record 标准。
- 生成全新补数。
- 判断复杂标签知识。
- 训练数据提交。
这些分别交给 `product-data``label-master` 或后续数据仓库 Skill。
@@ -0,0 +1,231 @@
# 08. label-master Skill 实现
![label-master Skill 实现](assets/08-label-master.png)
## 1. 定位
`label-master` 是标签知识和边界分析 Skill。
它不把“给 query 打标签”做成一个黑盒工具,而是让 Agent 逐步读取知识、比较候选、解释依据,并在必要时调用脚本校验最终输出格式。
典型链路:
```text
query / 标签边界问题 / target 校验需求
-> 读取决策流程
-> 读取候选召回索引
-> 找 2-5 个候选标签
-> 读取候选标签卡片
-> 命中混淆时读取边界卡
-> 判断 complex / 多指令 / 自动任务等结构维度
-> 判断输出形态
-> 给出推荐、候选、依据、排除项和不确定点
-> 如要落数据,调用 validate_label_output.py
```
位置:
```text
skills/label-master/SKILL.md
skills/label-master/knowledge/
skills/label-master/scripts/
```
## 2. 知识组织
核心目录:
```text
knowledge/
标签总览.md
决策流程.md
索引/
候选召回索引.md
标签索引.md
维度索引.md
label_manifest.json
判断维度/
复杂度/
多指令/
自动任务/
标注输出形态.md
输出能力/
标签/
边界/
边界索引.md
高频混淆/
领域概览/
迁移记录.md
```
设计原则:
```text
索引先行
不直接读全量标签卡。
维度分离
complex、多指令、自动任务不是业务标签。
标签卡片中文维护
方便人工编辑。
输出能力独立
function、intent、object、Agent 包装放在输出能力层。
边界卡优先人工维护
高频混淆写清楚,不只依赖自动迁移总结。
```
## 3. Agent 使用方式
`label-master` 的关键在于利用 Agent 的多轮阅读和规划能力。
典型 Agent loop
```text
模型选择 label-master
-> read_file knowledge/决策流程.md
-> read_file knowledge/索引/候选召回索引.md
-> read_file 候选标签卡片
-> read_file 高频混淆边界卡
-> 必要时 read_file 判断维度/复杂度/*
-> 必要时 read_file 输出能力/*
-> 输出可 review 判断
```
为什么不做成单个 `classify_query(query) -> label` 工具:
- 标签判断常常需要比较候选和排除项。
- function / intent / Agent 包装要看迁移状态。
- complex、多指令、自动任务是独立维度。
- 人类 review 需要看到依据,而不是只看到最终标签。
因此脚本只做索引和校验,不替代语义判断。
## 4. 输出形态
标签知识中存在多种输出形态:
```text
Agent(tag="xxx")
function program
intent
object + function 组合
多指令 JSON
自动任务 JSON
complex=true/false
```
`complex` 是独立维度,不应该混在 target 里。
例如训练输出可以组合成:
```text
complex=false
Agent(tag="地图导航")
```
但内部判断要拆成:
```text
complex: false
target: Agent(tag="地图导航")
```
## 5. 脚本能力
### 5.1 build_label_manifest.py
用途:
```text
把 Markdown 知识生成机器索引。
```
位置:
```text
skills/label-master/scripts/build_label_manifest.py
```
输出:
```text
skills/label-master/knowledge/索引/label_manifest.json
```
使用场景:
- 新增标签卡。
- 修改输出能力。
- 修改判断维度。
- 修改边界知识。
### 5.2 validate_label_output.py
用途:
```text
校验 target/function/intent/Agent/多指令/自动任务输出格式。
```
位置:
```text
skills/label-master/scripts/validate_label_output.py
```
典型调用:
```text
python_exec(script_path="skills/label-master/scripts/validate_label_output.py", args=["--target", "Agent(tag=\"地图导航\")"])
```
批量校验:
```text
--file output/records.jsonl --field target
```
## 6. 和 product-data 的关系
`product-data` 在需要确认 target 或生成边界数据时,可以读取 `label-master` 的知识。
分工:
```text
label-master
判断标签知识、边界、输出形态、target 合法性。
product-data
做数据生成 review、draft、canonical records、导出。
```
当用户给出模糊标签名时,应该先用 `label-master` 辅助确认:
```text
标签是否存在
使用 Agent 包装还是 function
complex 默认是什么
是否有高频混淆边界
```
再进入 `product-data` 的生成计划。
## 7. 设计边界
`label-master` 负责:
- 标签知识检索。
- 候选标签比较。
- 边界解释。
- 输出形态判断。
- target 格式校验。
不负责:
- 生成数据集。
- 线上日志挖掘。
- 导出训练/评测格式。
- 直接替用户确认争议边界。
@@ -0,0 +1,186 @@
# 09. 外部系统 SkillELK、SQL、模型迭代
![外部系统 Skill 接入模式](assets/09-external-skills.png)
## 1. 这类 Skill 的共同特征
外部系统 Skill 的核心不是复杂 prompt,而是把某个内部系统的调用方式、参数约定、依赖和输出格式打包起来。
典型形态:
```text
SKILL.md
写清楚什么时候用、参数怎么选、哪些动作需要确认。
scripts/
执行真实外部系统调用。
python_exec
作为统一执行入口。
python_package
处理依赖缺失。
output/
查询结果或报表写入当前 session output。
```
## 2. elk-fetch
位置:
```text
skills/elk-fetch/SKILL.md
skills/elk-fetch/elk_query.py
```
定位:
```text
按 request id 或查询条件读取小米内网 ELK 日志。
```
实现方式:
- `SKILL.md` 写明支持的 profile 和查询方式。
- 实际执行必须使用 `python_exec.script_path`
-`elasticsearch``urllib3` 时用 `python_package`
- 不让模型用 bash 手写临时 Python 查询脚本。
典型价值:
```text
把“怎么查 ELK”这类个人经验变成团队共享能力。
```
`online-mining-v2` 的区别:
```text
elk-fetch
更偏单次日志查询和调试。
online-mining-v2
更偏批量挖掘、策略迭代、样本转换。
```
## 3. data-factory-sql
位置:
```text
skills/data-factory-sql/SKILL.md
skills/data-factory-sql/run_sql.py
```
定位:
```text
通过 Kyuubi HTTP API 执行数据工场 SQL,轮询状态并下载 CSV 结果。
```
实现方式:
- 用户直接给 SQL 时,可以确认后执行。
- 用户只给分析需求时,Agent 可以先生成 SQL 草稿,再让用户确认。
- 执行必须通过 `python_exec.script_path`
- 输出默认写入当前 session output。
这个 Skill 的门禁重点是:
```text
SQL 执行前确认。
控制查询范围和 limit。
输出路径清晰。
失败时展示错误和可调整建议。
```
## 4. model-iteration
位置:
```text
skills/model-iteration/SKILL.md
skills/model-iteration/scripts/
```
定位:
```text
模型训练、评估、数据准备和迭代流程辅助。
```
这类 Skill 属于混合工程型:
- 有流程。
- 有脚本。
- 有配置。
- 可能调用外部训练平台。
- 高风险动作较多。
因此它更需要明确:
```text
哪些步骤只是分析。
哪些步骤会启动训练。
哪些步骤需要用户确认。
输出目录和实验记录在哪里。
```
## 5. 外部 Skill 的通用约定
### 5.1 调用方式
优先:
```json
{
"script_path": "skills/<skill-name>/scripts/run.py",
"stdin": "{...}",
"timeout_seconds": 120
}
```
或者脚本在 Skill 根目录时:
```json
{
"script_path": "skills/elk-fetch/elk_query.py",
"args": ["..."]
}
```
### 5.2 依赖处理
依赖缺失时:
```text
python_package(action="install", packages=[...])
```
不要:
```text
bash pip install ...
bash python ...
```
### 5.3 输出处理
外部系统查询结果应该:
- stdout 给结构化摘要。
- 大结果写入 `output/``scratchpad/`
- 返回实际文件路径。
- 避免把大量数据直接塞进最终回复。
### 5.4 安全和确认
需要确认的动作:
- 执行大范围 SQL。
- 启动训练。
- 写外部路径。
- 推送代码或数据仓库。
- 导出可能包含敏感字段的线上数据。
只读、小范围、用户明确指定 rid 或 SQL 的查询,可以直接执行,但仍要控制结果规模。
@@ -0,0 +1,59 @@
# ZK Data Agent 技术架构说明
这组文档先服务于技术 review 和后续讲解材料沉淀,不做宣传表达,不做产品比较。
每个章节尽量对应当前代码里的真实模块、函数和 Skill 实现,后续 `/doc` 页面可以从这里抽取内容做视觉化呈现。
## 阅读顺序
1. [基座 Runtime 架构](01-base-runtime.md)
2. [Agent Loop 执行机制](02-agent-loop.md)
3. [工具体系和 Tool Handler](03-tools.md)
4. [Skill 体系和能力包约定](04-skills.md)
5. [会话工作区、运行态和记忆](05-workspace-memory-observability.md)
6. [product-data Skill 实现](06-product-data.md)
7. [online-mining-v2 Skill 实现](07-online-mining-v2.md)
8. [label-master Skill 实现](08-label-master.md)
9. [外部系统 Skill:ELK、SQL、模型迭代](09-external-skills.md)
## 一句话定位
ZK Data Agent 的基座是一个 Web 化、多用户、可观测的 Agent runtime。
业务能力通过 `skills/<skill-name>/SKILL.md``knowledge/``scripts/` 组织;稳定执行能力通过 Tool handler 或 Skill 内 portable scripts 承载;每次运行通过 Agent loop 让模型在“判断、调用工具、观察结果、继续判断”之间循环。
## 当前技术主线
```text
Web UI
-> backend/api/server.py
-> LocalCodingAgent
-> agent_prompting 组装系统提示词和 Skill 列表
-> OpenAICompatClient 调模型
-> 模型返回文本或 tool_calls
-> agent_tools 执行 handler
-> session workspace 保存 input/scratchpad/output/session.json
-> run_state_store / event stream 推给前端活动区
-> personal_memory 后台异步整理用户记忆和 Skill 记忆
```
## 代码入口速查
| 主题 | 主要代码 |
|------|----------|
| Agent runtime | `src/agent_runtime.py` |
| 系统提示词 | `src/agent_prompting.py` |
| Tool registry / handler | `src/agent_tools.py``src/agent_tool_specs/` |
| Skill loader | `src/bundled_skills.py` |
| 模型兼容层 | `src/openai_compat.py` |
| Web 后端 | `backend/api/server.py` |
| 会话持久化 | `src/agent_session.py``src/session_store.py` |
| 记忆后台 | `src/personal_memory.py` |
| 数据生成 Skill | `skills/product-data/` |
| 线上挖掘 Skill | `skills/online-mining-v2/` |
| 标签知识 Skill | `skills/label-master/` |
## 后续维护原则
- 如果是在讲“基座怎么运行”,优先改 01-05。
- 如果是在讲“某个 Skill 怎么做事”,优先改 06-09。
- 如果代码实现发生变化,先更新对应章节,再考虑同步 README 或 `/doc` 页面。
- 页面表达、图形素材、演示脚本不写在这里;这里只保留技术事实和设计边界。
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB