diff --git a/README.md b/README.md index 79b5eac..c9d72d0 100644 --- a/README.md +++ b/README.md @@ -583,6 +583,22 @@ bash "$HOME/zk-data-agent/scripts/install-from-git.sh" - 安装前端依赖并构建。 - 安装并启动用户级 systemd 服务。 +如果要启用“平台账号 = Linux 用户”的托管工作区,需要使用 root/systemd system 服务部署: + +```bash +cd "$HOME/zk-data-agent" +sudo -E env PATH="$PATH" bash scripts/deploy-ubuntu.sh main --system-service --enable-linux-accounts +``` + +启用后: + +- 平台注册/登录账号时会同步创建同名 Linux 用户。 +- 平台密码会同步设置为 Linux 用户密码。 +- Linux 用户允许 SSH 登录。 +- 本机托管工作区会使用 `/home//zk-agent/`。 +- `python_exec`、`python_package`、`bash` 会在对应 Linux 用户身份下执行。 +- Jupyter 远程工作区保持现有逻辑,不参与本机 Linux 用户隔离。 + `.env.deploy` 会保存: ```text @@ -595,6 +611,8 @@ CLAW_BACKEND_PORT CLAW_FRONTEND_HOST CLAW_FRONTEND_PORT CLAW_API_URL +CLAW_SERVICE_SCOPE +CLAW_ENABLE_LINUX_ACCOUNTS CLAW_NPM_BIN CLAW_NPX_BIN CLAW_NODE_BIN @@ -618,6 +636,12 @@ bash scripts/update-server-fast.sh bash scripts/deploy-ubuntu.sh ``` +root/system 服务更新: + +```bash +sudo -E env PATH="$PATH" bash scripts/deploy-ubuntu.sh main --system-service --enable-linux-accounts +``` + 部署指定分支: ```bash @@ -632,7 +656,7 @@ bash scripts/deploy-ubuntu.sh main --force ### 服务管理 -部署脚本会安装两个用户级 systemd 服务: +部署脚本默认安装用户级 systemd 服务;以 root 执行或指定 `--system-service` 时安装 system 级服务。服务名默认是: ```text zk-data-agent-backend @@ -646,6 +670,13 @@ systemctl --user status zk-data-agent-backend systemctl --user status zk-data-agent-frontend ``` +system 级服务使用: + +```bash +systemctl status zk-data-agent-backend +systemctl status zk-data-agent-frontend +``` + 查看日志: ```bash @@ -653,12 +684,25 @@ journalctl --user -u zk-data-agent-backend -f journalctl --user -u zk-data-agent-frontend -f ``` +system 级日志使用: + +```bash +journalctl -u zk-data-agent-backend -f +journalctl -u zk-data-agent-frontend -f +``` + 重启服务: ```bash systemctl --user restart zk-data-agent-backend zk-data-agent-frontend ``` +system 级重启使用: + +```bash +systemctl restart zk-data-agent-backend zk-data-agent-frontend +``` + 停止服务: ```bash @@ -683,6 +727,7 @@ Ubuntu 部署建议: - Python `3.10.14` - Node.js `20` 或 `22` - `npm` +- 启用 Linux 账号工作区时,还需要 `passwd`、`python3`、`python3-venv`,并要求服务以 root/systemd system 方式运行。 首次安装如果缺 Python 编译依赖,可以执行: @@ -690,7 +735,7 @@ Ubuntu 部署建议: bash scripts/deploy-ubuntu.sh --bootstrap-system ``` -`--bootstrap-system` 会使用 `sudo apt-get` 安装系统依赖;应用代码、虚拟环境、前端依赖、运行数据和 systemd 用户服务仍然位于当前用户目录。 +`--bootstrap-system` 会使用 `sudo apt-get` 安装系统依赖;普通模式下应用代码、虚拟环境、前端依赖、运行数据和 systemd 用户服务仍然位于当前用户目录。启用 `--enable-linux-accounts` 后,账号工作区位于 `/home//zk-agent/`。 ## 开发验证 diff --git a/backend/api/server.py b/backend/api/server.py index 99fe8fb..48b95ef 100644 --- a/backend/api/server.py +++ b/backend/api/server.py @@ -13,6 +13,7 @@ import hashlib import shlex import json import os +import pwd import queue import re import shutil @@ -76,6 +77,9 @@ from src.session_store import ( ) from src.token_budget import calculate_token_budget +LINUX_ACCOUNT_WORKSPACE_NAME = 'zk-agent' +LINUX_ACCOUNT_ENV = 'CLAW_ENABLE_LINUX_ACCOUNTS' + STATIC_DIR = Path(__file__).resolve().parents[2] / 'frontend' / 'legacy-static' API_TOOL_CONTENT_MAX_CHARS = 20000 @@ -638,6 +642,8 @@ class AgentState: if not account_id: return self.session_directory.parent safe_id = _safe_account_id(account_id) + if _linux_accounts_enabled(): + return _linux_account_workspace(safe_id).resolve() return (self.session_directory.parent / 'accounts' / safe_id).resolve() def account_paths(self, account_id: str | None) -> dict[str, Path]: @@ -837,7 +843,15 @@ class AgentState: for directory in paths.values(): if directory.name != '.venv': directory.mkdir(parents=True, exist_ok=True) - self._ensure_python_env(paths['python_env']) + runtime_user = ( + _safe_account_id(account_id) + if account_id and _linux_accounts_enabled() + else None + ) + if runtime_user: + _ensure_linux_user_exists(runtime_user) + _chown_path_for_linux_user(paths['base'], runtime_user, recursive=True) + self._ensure_python_env(paths['python_env'], account_id) permissions = AgentPermissions( allow_file_write=config.allow_write, allow_shell_commands=config.allow_shell, @@ -849,6 +863,7 @@ class AgentState: session_directory=paths['sessions'], scratchpad_root=paths['scratchpad'], python_env_dir=paths['python_env'], + runtime_user=runtime_user, enabled_skill_names=self.enabled_skill_names(account_id), auto_compact_threshold_tokens=180_000, ) @@ -949,12 +964,37 @@ class AgentState: def lock(self) -> Lock: return self._lock - def _ensure_python_env(self, env_dir: Path) -> None: + def _ensure_python_env(self, env_dir: Path, account_id: str | None = None) -> None: python_bin = env_dir / 'bin' / 'python' pip_bin = env_dir / 'bin' / 'pip' if python_bin.exists() and pip_bin.exists(): return env_dir.parent.mkdir(parents=True, exist_ok=True) + runtime_user = _safe_account_id(account_id) if account_id and _linux_accounts_enabled() else None + if runtime_user: + _ensure_linux_user_exists(runtime_user) + env_dir.parent.mkdir(parents=True, exist_ok=True) + _chown_path_for_linux_user(env_dir.parent, runtime_user, recursive=True) + runtime_python = os.environ.get('CLAW_RUNTIME_PYTHON_BIN') or shutil.which('python3') + if not runtime_python: + raise RuntimeError('Linux runtime requires python3 or CLAW_RUNTIME_PYTHON_BIN') + subprocess.run( + [ + 'runuser', + '-u', + runtime_user, + '--', + runtime_python, + '-m', + 'venv', + str(env_dir), + ], + check=True, + capture_output=True, + text=True, + timeout=120, + ) + return venv.EnvBuilder(with_pip=True, symlinks=False).create(env_dir) @@ -4538,6 +4578,107 @@ def _account_id_from_email(email: str | None) -> str | None: return prefix +def _linux_accounts_enabled() -> bool: + return os.environ.get(LINUX_ACCOUNT_ENV, '').strip().lower() in { + '1', + 'true', + 'yes', + 'on', + } + + +def _linux_account_workspace(account_id: str) -> Path: + return Path('/home') / account_id / LINUX_ACCOUNT_WORKSPACE_NAME + + +def _validate_linux_username(account_id: str) -> None: + if not re.fullmatch(r'[a-z_][a-z0-9_-]{1,31}', account_id): + raise HTTPException( + status_code=400, + detail='启用 Linux 账号时,账号名必须以小写字母或下划线开头,只包含小写字母、数字、下划线或中划线,长度 2-32', + ) + + +def _ensure_linux_account(account_id: str, password: str) -> None: + _validate_linux_username(account_id) + if os.geteuid() != 0: + raise HTTPException( + status_code=500, + detail='CLAW_ENABLE_LINUX_ACCOUNTS=1 需要后端以 root 身份运行', + ) + try: + pwd.getpwnam(account_id) + except KeyError: + subprocess.run( + [ + 'useradd', + '-m', + '-d', + f'/home/{account_id}', + '-s', + '/bin/bash', + account_id, + ], + check=True, + capture_output=True, + text=True, + timeout=30, + ) + chpasswd = subprocess.run( + ['chpasswd'], + input=f'{account_id}:{password}\n', + check=False, + capture_output=True, + text=True, + timeout=30, + ) + if chpasswd.returncode != 0: + detail = (chpasswd.stderr or chpasswd.stdout or '').strip() + raise HTTPException(status_code=500, detail=f'同步 Linux 用户密码失败:{detail}') + workspace = _linux_account_workspace(account_id) + for child in ( + workspace / 'sessions', + workspace / 'python', + workspace / 'memory', + workspace / 'integrations', + ): + child.mkdir(parents=True, exist_ok=True) + _chown_path_for_linux_user(workspace, account_id, recursive=True) + + +def _ensure_linux_user_exists(account_id: str) -> None: + try: + pwd.getpwnam(account_id) + except KeyError as exc: + raise RuntimeError( + f'Linux runtime user does not exist: {account_id}. ' + 'Create the account through the platform first, or disable CLAW_ENABLE_LINUX_ACCOUNTS.' + ) from exc + + +def _chown_path_for_linux_user(path: Path, account_id: str, *, recursive: bool = False) -> None: + if os.name != 'posix': + return + try: + user_info = pwd.getpwnam(account_id) + except KeyError: + return + target = path.resolve(strict=False) + try: + os.chown(target, user_info.pw_uid, user_info.pw_gid) + except PermissionError: + return + except FileNotFoundError: + return + if not recursive or not target.is_dir(): + return + for child in target.rglob('*'): + try: + os.chown(child, user_info.pw_uid, user_info.pw_gid) + except (PermissionError, FileNotFoundError): + continue + + def _admin_token() -> str: return os.environ.get('ZK_ADMIN_TOKEN') or 'admin' @@ -4686,6 +4827,8 @@ def _admin_create_account(state: AgentState, account_id: str) -> dict[str, Any]: safe_id = _safe_account_id(account_id) if safe_id == 'default': raise HTTPException(status_code=400, detail='账号名不合法') + if _linux_accounts_enabled(): + _validate_linux_username(safe_id) users = _load_users_file(state) user_rows = users.setdefault('users', []) if any( @@ -4694,6 +4837,8 @@ def _admin_create_account(state: AgentState, account_id: str) -> dict[str, Any]: for item in user_rows ): raise HTTPException(status_code=400, detail='账号已存在') + if _linux_accounts_enabled(): + _ensure_linux_account(safe_id, '123456') user_rows.append( { 'id': safe_id, @@ -4737,8 +4882,18 @@ def _admin_delete_account(state: AgentState, account_id: str) -> dict[str, Any]: encoding='utf-8', ) base = _accounts_root(state) / safe_id + if _linux_accounts_enabled(): + base = _linux_account_workspace(safe_id) if base.exists(): shutil.rmtree(base) + if _linux_accounts_enabled(): + subprocess.run( + ['passwd', '-l', safe_id], + check=False, + capture_output=True, + text=True, + timeout=30, + ) state._clear_agents_for_account(safe_id) return {'account_id': safe_id, 'deleted': True} diff --git a/docs/technical-architecture/01-base-runtime.md b/docs/technical-architecture/01-base-runtime.md new file mode 100644 index 0000000..825d65e --- /dev/null +++ b/docs/technical-architecture/01-base-runtime.md @@ -0,0 +1,221 @@ +# 01. 基座 Runtime 架构 + +![基座 Runtime 架构](assets/01-base-runtime.png) + +## 1. 项目定位 + +ZK Data Agent 的定位不是通用聊天机器人,也不是单人本地 IDE Agent,而是面向团队业务流程的 Web Agent 工作台。 + +它基于已有的本地工程 Agent 能力继续往上搭: + +- `LocalCodingAgent` 提供多轮 Agent Loop。 +- OpenAI-compatible client 提供模型调用适配。 +- Tool registry 和 handler 提供可控执行能力。 +- Session workspace 提供每个会话独立的输入、中间文件和产物目录。 +- Skill system 把流程协议、业务知识、脚本和样例打包成可复用能力。 +- Memory worker 把用户偏好和 Skill 使用经验异步整理为可编辑记忆。 + +这个项目要解决的核心痛点是:团队里的数据开发、线上挖掘、标签判断等流程,往往散在口头经验、临时 prompt、个人脚本、聊天记录和本地文件里。每个人都能临时做一次,但很难让别人稳定复用、持续维护、形成可审计的产物链。 + +当前已经验证过的主要业务场景包括: + +- `product-data`:从产品定义、标签边界、样例 query 或 badcase 出发,生成 canonical records,并导出流转 CSV、训练 JSONL、评测 CSV。 +- `online-mining-v2`:基于 ELK 日志挖掘线上样本,保留 request_id、timestamp、session、模型 prompt、模型输出、domain 等信息,并接入后续数据规范。 +- `label-master`:把复杂度、多指令、自动任务、标签定义、function 输出和边界经验整理为可检索知识体系。 +- 外部系统 Skill:ELK、SQL、模型迭代、飞书在线文档转换等能力以 Skill 方式接入,不侵入基座。 + +因此,基座 Runtime 的目标不是把某一条业务链路写死,而是提供一套稳定的承载层,让不同业务能力都能以 Skill 的方式运行、交付、沉淀和更新。 + +## 2. 基座解决的问题 + +基座不绑定某一个业务流程。它提供的是通用 Agent runtime: + +- 多用户 Web 入口。 +- 多会话状态管理。 +- 模型选择和 OpenAI-compatible 调用。 +- Tool registry 和 tool handler 执行。 +- Skill 发现、启用和提示词注入。 +- 当前会话工作区。 +- 运行态、事件流、活动区和刷新恢复。 +- 用户记忆和 Skill 记忆后台。 + +业务能力例如 `product-data`、`online-mining-v2`、`label-master` 都跑在这个基座上。 + +## 3. 文字架构图 + +```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 注入 +``` + +## 4. 关键代码入口 + +### 4.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 +``` + +### 4.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(...) +``` + +### 4.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 +``` + +## 5. 基座的分层职责 + +```text +Web UI + 负责交互、展示、文件面板、活动区、Skill 勾选、管理后台。 + +Backend API + 负责账号、会话、模型配置、运行态、服务端事件流。 + +Agent Runtime + 负责 Agent loop、模型调用、工具调用、预算和持久化。 + +Prompting + 负责把规则、工具、公约、Skill 列表转成模型可见上下文。 + +Tool Runtime + 负责稳定执行动作,并把结果结构化回传给模型。 + +Skill System + 负责让业务流程、知识、脚本可被 Agent 发现和使用。 + +Workspace + 负责隔离每个用户和每个会话的输入、临时文件和交付产物。 + +Memory Worker + 负责从交互历史中异步整理长期偏好和 Skill 使用经验。 +``` + +## 6. 设计边界 + +基座只应该承载跨业务复用的稳定能力,例如: + +- 工具执行。 +- 会话状态。 +- 运行态事件。 +- 工作区路径。 +- Skill 发现和启用。 +- 模型适配。 +- 记忆后台。 + +业务流程不应该写死在基座里。数据生成、线上挖掘、标签判断等变化快的流程应该沉到 Skill;确定性脚本应该放在对应 Skill 的 `scripts/` 下。 diff --git a/docs/technical-architecture/02-agent-loop.md b/docs/technical-architecture/02-agent-loop.md new file mode 100644 index 0000000..bd17007 --- /dev/null +++ b/docs/technical-architecture/02-agent-loop.md @@ -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。 diff --git a/docs/technical-architecture/03-tools.md b/docs/technical-architecture/03-tools.md new file mode 100644 index 0000000..6f3eb91 --- /dev/null +++ b/docs/technical-architecture/03-tools.md @@ -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 要尽量简单,否则不同模型后端可能不兼容。 diff --git a/docs/technical-architecture/04-skills.md b/docs/technical-architecture/04-skills.md new file mode 100644 index 0000000..ceaf3fe --- /dev/null +++ b/docs/technical-architecture/04-skills.md @@ -0,0 +1,256 @@ +# 04. Skill 体系和能力包约定 + +![Skill 体系和能力包约定](assets/04-skills.png) + +## 1. Skill 的定位 + +Skill 是经验层。它不是单纯 prompt,也不是单纯脚本。 + +一个 Skill 应该回答: + +- 什么场景触发。 +- 输入材料是什么。 +- Agent 应该按什么流程做。 +- 哪些地方必须让用户 review。 +- 应该调用哪些工具或脚本。 +- 产物应该写到哪里。 +- 哪些做法是禁止的。 + +稳定可执行逻辑不应该长期写在 Skill 文本里,而应该进入: + +```text +skills//scripts/ +``` + +或者平台级工具。 + +## 2. Skill loader 实现 + +关键文件: + +```text +src/bundled_skills.py +``` + +项目级 Skill 目录: + +```text +skills//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.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 +``` diff --git a/docs/technical-architecture/05-workspace-memory-observability.md b/docs/technical-architecture/05-workspace-memory-observability.md new file mode 100644 index 0000000..4255311 --- /dev/null +++ b/docs/technical-architecture/05-workspace-memory-observability.md @@ -0,0 +1,243 @@ +# 05. 会话工作区、运行态和记忆 + +![会话工作区、运行态和记忆](assets/05-workspace-memory-observability.png) + +## 1. 会话工作区 + +每个用户、每个会话都有独立目录: + +```text +.port_sessions/accounts//sessions// + 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/.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/.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 做了什么、文件在哪里、失败在哪个工具或阶段。 diff --git a/docs/technical-architecture/06-product-data.md b/docs/technical-architecture/06-product-data.md new file mode 100644 index 0000000..cfb5096 --- /dev/null +++ b/docs/technical-architecture/06-product-data.md @@ -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//sessions//output/ +``` + +Skill 明确禁止: + +- 按数据集名创建随机子目录。 +- 把 records 写到项目根目录。 +- 用 `write_file` 手写最终 JSONL/CSV。 +- 在未确认 plan 前生成数据。 + +## 9. 设计边界 + +`product-data` 负责: + +- 数据目标对齐。 +- 生成计划 review。 +- dataset draft 生成协议。 +- canonical records 转换和导出。 + +不负责: + +- 判断所有标签知识。 +- 查询线上数据。 +- ELK 检索。 +- 模型训练或 git 数据仓库提交。 + +标签知识应由 `label-master` 辅助,线上数据由 `online-mining-v2` 或相关 Skill 获取。 diff --git a/docs/technical-architecture/07-online-mining-v2.md b/docs/technical-architecture/07-online-mining-v2.md new file mode 100644 index 0000000..703495b --- /dev/null +++ b/docs/technical-architecture/07-online-mining-v2.md @@ -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。 diff --git a/docs/technical-architecture/08-label-master.md b/docs/technical-architecture/08-label-master.md new file mode 100644 index 0000000..90a749a --- /dev/null +++ b/docs/technical-architecture/08-label-master.md @@ -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 格式校验。 + +不负责: + +- 生成数据集。 +- 线上日志挖掘。 +- 导出训练/评测格式。 +- 直接替用户确认争议边界。 diff --git a/docs/technical-architecture/09-external-skills.md b/docs/technical-architecture/09-external-skills.md new file mode 100644 index 0000000..f4099c2 --- /dev/null +++ b/docs/technical-architecture/09-external-skills.md @@ -0,0 +1,186 @@ +# 09. 外部系统 Skill:ELK、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//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 的查询,可以直接执行,但仍要控制结果规模。 diff --git a/docs/technical-architecture/10-memory-research.md b/docs/technical-architecture/10-memory-research.md new file mode 100644 index 0000000..72859bd --- /dev/null +++ b/docs/technical-architecture/10-memory-research.md @@ -0,0 +1,478 @@ +# 10. Agent 记忆机制调研与 ZK Data Agent 对比 + +本文整理主流 Agent / AI 产品的记忆实现方式,并对照 ZK Data Agent 当前实现。目标不是判断哪一种“最好”,而是说明不同记忆机制分别解决什么问题,以及为什么我们当前选择“用户记忆 + Skill 使用记忆 + 异步整理队列 + Markdown 可编辑文件”的路线。 + +调研时间:2026-05-19。 + +## 1. 结论摘要 + +主流记忆实现大致分为六类: + +| 类型 | 代表 | 核心做法 | 适合场景 | +|------|------|----------|----------| +| 产品级长期记忆 | ChatGPT Memory | 平台自动保存用户偏好和事实,并在后续对话中注入 | 通用个人助手 | +| 会话状态记忆 | OpenAI Agents SDK Sessions、AutoGen Memory | 自动保存历史消息或把外部记忆注入上下文 | 线程连续对话 | +| 文件化项目记忆 | Claude Code `CLAUDE.md`、Claw 基座 memory files | 通过项目/用户级 Markdown 文件向 Agent 注入稳定规则 | 工程项目、团队约定 | +| 图/向量检索记忆 | LangGraph Store、Mem0、Zep/Graphiti | 抽取事实,存入向量库或知识图谱,按语义检索 | 长期、跨会话、海量事实 | +| Agent 自主管理记忆 | Letta / MemGPT | Agent 有显式 memory blocks 和 archival memory,可读写管理 | 状态型 Agent、长期角色 | +| 框架内置任务记忆 | CrewAI | 短期、长期、实体、上下文记忆组合 | 多 Agent 任务协作 | + +ZK Data Agent 当前更接近: + +```text +文件化项目记忆 + + 产品级用户记忆 + + Skill 作用域记忆 + + 异步记忆整理队列 +``` + +它没有优先做向量库或知识图谱,而是选择 Markdown 文件作为最终记忆正文。这个取舍适合当前团队场景:记忆内容需要能被用户看到、编辑、删除,并且要按 Skill 作用域精准注入。 + +## 2. 主流实现机制 + +### 2.1 ChatGPT Memory:产品级个人长期记忆 + +ChatGPT Memory 的核心是平台级用户记忆。它会保存用户偏好、事实和历史对话中有持续价值的信息,并在后续对话中使用。用户可以查看、管理、删除保存的记忆,也可以关闭相关能力。 + +机制特点: + +- 记忆作用域是用户账号。 +- 由产品后台判断哪些内容值得保存。 +- 注入方式对用户透明,用户看到的是“助手更了解我”。 +- 适合通用个人助手,不适合表达复杂业务流程结构。 + +和我们的关系: + +ZK Data Agent 的“用户记忆”借鉴了这个方向,但没有把全部记忆做成黑盒。我们把最终正文落到 `user.md`,并在 UI 里允许用户编辑。 + +### 2.2 OpenAI Agents SDK Sessions:会话状态记忆 + +OpenAI Agents SDK 的 Sessions 主要解决“同一个会话线程里自动保留历史上下文”。开发者不需要每轮手动传入完整历史,Session 会保存对话项,并在下一轮运行时自动带上。 + +机制特点: + +- 更偏 conversation state,而不是长期个人偏好。 +- 适合多轮会话连续执行。 +- 常见实现是 SQLite / SQLAlchemy / 自定义 session backend。 +- 记忆对象主要是消息历史,不是抽象后的长期知识。 + +和我们的关系: + +ZK Data Agent 也有 session 持久化,但我们把它和“长期记忆”分开: + +```text +session.json + 保存当前会话消息、工具调用、产物和运行事件。 + +memory/user.md、memory/skills/*.md + 保存跨会话长期偏好和 Skill 使用经验。 +``` + +这个区分很重要:会话历史服务“恢复当前任务”,长期记忆服务“下次任务更懂用户和业务”。 + +### 2.3 Claude Code / OpenClaw / Claw:文件化项目记忆 + +Claude Code 使用 `CLAUDE.md` 作为项目或用户级记忆文件,常用于保存仓库规则、构建命令、代码风格、项目约定等。OpenClaw / Claw 类 Code Agent 基座通常也会保留这条路线:从全局或工作目录发现记忆文件,并把内容注入上下文。 + +在当前仓库里,对应实现主要是: + +```text +src/agent_context.py +src/session_memory_compact.py +``` + +其中 `agent_context.py` 负责发现全局和目录级 memory files,`session_memory_compact.py` 负责会话压缩场景下的 session memory 摘要。 + +机制特点: + +- 记忆是文本文件,天然可读、可版本化。 +- 非常适合工程项目规则和团队约定。 +- 注入通常按目录/项目作用域进行。 +- 记忆更新更多依赖人工维护,而不是完全自动。 + +和我们的关系: + +ZK Data Agent 继承了“文件化、可编辑、可解释”的优点,但把作用域进一步细分: + +```text +user.md + 用户级偏好和稳定习惯。 + +skills/.md + 某个 Skill 的使用经验、踩坑、格式偏好和边界修正。 +``` + +也就是说,我们不是只有“项目记忆”,而是增加了“Skill 记忆”这一层。 + +### 2.4 LangGraph:线程状态 + 长期 Memory Store + +LangGraph 把 memory 分成 short-term memory 和 long-term memory。短期记忆通常跟 thread 绑定,用来维持一次会话;长期记忆通过 store 按 namespace 保存,可以跨 thread 召回。它还把长期记忆进一步拆成 semantic、episodic、procedural 等类型。 + +机制特点: + +- thread state 解决会话内上下文。 +- store 解决跨会话长期信息。 +- 支持按 user id / namespace 组织记忆。 +- 长期记忆可以由应用逻辑或 Agent 写入、搜索、更新。 + +和我们的关系: + +ZK Data Agent 当前没有引入通用 Store / VectorStore,而是用文件系统和 SQLite 队列实现一个轻量版本: + +```text +namespace = account_id + memory kind + skill_name +storage = Markdown files + SQLite queue +retrieval = user memory always considered, skill memory按启用 Skill 精准注入 +``` + +这比 LangGraph Store 简单,但更直接服务我们当前的 Skill 工作台。 + +### 2.5 Mem0:独立记忆层 + +Mem0 更像一个独立 memory layer。典型链路是:从对话中抽取事实,存入记忆系统;后续根据 query 检索相关记忆,再注入给模型。它强调 add / search / update / delete 这类记忆 API,也支持面向用户、Agent、session 等维度组织。 + +机制特点: + +- 记忆层和 Agent 框架解耦。 +- 常见存储后端是向量、图或混合检索。 +- 强调自动抽取、去重、更新和语义召回。 +- 适合大规模个性化 Agent 或跨应用记忆服务。 + +和我们的关系: + +ZK Data Agent 目前没有把记忆做成独立检索服务。原因是我们的高频需求不是“从海量事实里语义搜索”,而是“把少量稳定经验准确注入到对应 Skill”。如果未来 Skill 记忆膨胀,可以在 Markdown 之外增加 Mem0 类似的检索层。 + +### 2.6 Letta / MemGPT:Agent 自主管理内存 + +Letta 延续 MemGPT 思路,把 Agent 看成有长期状态的主体。它通常区分 core memory 和 archival memory:core memory 是短小、常驻上下文的重要信息;archival memory 是更大的外部记忆空间,Agent 可以通过工具读写。 + +机制特点: + +- Agent 可以主动管理自己的记忆。 +- core memory 常驻,archival memory 需要检索。 +- 适合长期角色 Agent、个人助理、需要自我状态连续性的 Agent。 +- 复杂度更高,需要更强的记忆写入约束和审计。 + +和我们的关系: + +ZK Data Agent 没有让主 Agent 在执行链路里自由修改记忆。我们把记忆写入放到后台 worker,避免主任务因为记忆整理变慢或出错。这是一个更保守的团队平台取舍。 + +### 2.7 Zep / Graphiti:时间感知知识图谱记忆 + +Zep / Graphiti 代表的是 temporal knowledge graph 路线:从对话或事件中抽取实体和关系,形成带时间属性的知识图谱。它解决的问题不是简单偏好记忆,而是“事实如何随时间变化”“实体关系如何演进”。 + +机制特点: + +- 记忆结构是实体、关系、事件、时间。 +- 适合复杂事实网络和时间演化。 +- 检索结果可以包含关系路径和上下文。 +- 实现成本和运维复杂度高于 Markdown 或向量检索。 + +和我们的关系: + +标签边界、业务规则、Skill 使用经验目前更适合文本化规则,不一定需要图谱。但如果未来要做“用户、Skill、数据集、标签、错误类型、修复策略”之间的关系分析,图谱路线会有价值。 + +### 2.8 CrewAI:多 Agent 任务记忆 + +CrewAI 的记忆体系主要服务多 Agent 协作,通常包含 short-term memory、long-term memory、entity memory 和 contextual memory。它关注的是任务过程中多个 Agent 如何共享上下文和持续改进。 + +机制特点: + +- 和 Crew / Agent / Task 结构绑定。 +- 强调任务协作过程中的上下文复用。 +- 对实体、任务经验有独立组织方式。 + +和我们的关系: + +ZK Data Agent 当前不是多 Agent 编排优先,而是单个工作台 Agent + Skill 能力包优先。Skill 记忆在某种程度上承担了“任务经验记忆”的角色。 + +### 2.9 AutoGen:Memory 组件注入上下文 + +AutoGen 的 AgentChat 提供 Memory 抽象,可以把 list memory、vector memory 等组件挂到 AssistantAgent 上。运行时 Memory 会根据消息更新上下文,或把检索结果添加到模型输入。 + +机制特点: + +- Memory 是 Agent 可插拔组件。 +- 可以使用简单列表,也可以接向量检索。 +- 更偏框架扩展点,而不是产品级记忆管理 UI。 + +和我们的关系: + +ZK Data Agent 的记忆也可以理解为一个可插拔上下文组件,但我们额外做了用户 UI、Skill 作用域和后台队列。 + +## 3. ZK Data Agent 当前实现 + +实现入口: + +```text +src/personal_memory.py +backend/api/server.py +frontend/app/components/assistant-ui/threadlist-sidebar.tsx +``` + +### 3.1 存储结构 + +每个账号有独立记忆目录: + +```text +.port_sessions/accounts//memory/ + user.md + skills/ + .md + memory.db +``` + +其中: + +- `user.md`:用户级长期记忆。 +- `skills/.md`:某个 Skill 的使用记忆。 +- `memory.db`:事件队列、状态和 revision 账本。 + +### 3.2 注入逻辑 + +模型调用前,后端调用: + +```text +memory_manager.render_injection(account_id, enabled_skill_names) +``` + +注入规则: + +```text +用户记忆 + 账号级,作为长期偏好注入。 + +Skill 使用记忆 + 只读取当前启用 Skill 对应的 skills/.md。 + +冲突优先级 + 用户本轮明确要求 > 个性化记忆。 +``` + +这避免了一个常见问题:所有记忆都无差别注入导致上下文污染。 + +### 3.3 生成时机 + +每次交互结束后,后端调用: + +```text +memory_manager.enqueue_interaction(...) +``` + +系统不会每轮同步整理记忆,而是先检测信号: + +```text +显式记忆词: +记住、以后、下次、默认、总是、不要、应该、固定 + +纠错词: +不对、不是这样、格式错、之前说过、还是不行 + +Skill 经验: +skill、工具、流程、格式 + +工具经验: +模型返回的工具参数不是合法 JSON +``` + +命中后写入 SQLite pending 队列。显式记忆优先级更高。 + +### 3.4 异步整理 + +后台 worker 每 5 秒扫描账号事件,每次最多处理 8 条 pending event: + +```text +pending -> processing -> done / failed +``` + +整理方式: + +1. 读取已有 `user.md` 和相关 `skills/.md`。 +2. 把一批事件交给模型做“整理式合并”。 +3. 模型必须输出 JSON: + +```json +{ + "user_memory": "完整 Markdown 或空字符串", + "skill_memories": { + "skill-name": "完整 Markdown" + } +} +``` + +4. 如果模型输出不可解析,则走 fallback 规则。 +5. 写入 Markdown 文件,并更新 revision。 + +### 3.5 用户可编辑 + +前端左下角“记忆”入口支持: + +- 查看用户记忆行数。 +- 查看 Skill 记忆列表。 +- 编辑用户记忆。 +- 编辑某个 Skill 记忆。 +- 查看记忆队列状态。 + +管理后台只看队列、用量等统计,不展示其他用户具体记忆内容。 + +## 4. 对比表 + +| 维度 | ChatGPT | Claude Code / OpenClaw | LangGraph / Mem0 / Zep | Letta | ZK Data Agent | +|------|---------|--------------------|-------------------------|-------|---------------| +| 主要目标 | 个人助手个性化 | 项目规则注入 | 长期检索记忆 | 状态型长期 Agent | 团队 Skill 工作台 | +| 记忆粒度 | 用户 | 用户/项目/目录 | 用户/线程/实体/namespace | Agent memory block | 用户 + Skill | +| 存储形态 | 平台内部 | Markdown 文件 | Store / 向量 / 图 | Core + archival memory | Markdown + SQLite queue | +| 生成时机 | 产品后台自动 | 多为人工维护 | 自动抽取 / API 写入 | Agent 主动管理 | 交互结束后异步整理 | +| 检索方式 | 平台决定 | 直接注入文件 | 语义搜索 / 图检索 | Core 常驻 + archival 检索 | 用户记忆 + 当前 Skill 记忆注入 | +| 可编辑性 | 用户可管理 | 文件可编辑 | 取决于产品/API | 通常需要工具/API | UI 可编辑 Markdown | +| 适合业务流程沉淀 | 中 | 中 | 高,但工程复杂 | 高,但复杂 | 高,且轻量 | +| 风险 | 黑盒、难按业务作用域隔离 | 容易依赖人工维护 | 检索和更新复杂 | 主链路复杂度高 | 暂无语义召回和图谱能力 | + +## 5. 为什么当前方案适合我们 + +### 5.1 我们需要的是 Skill 使用经验,而不只是用户偏好 + +通用记忆多关注“用户是谁、用户喜欢什么”。我们的高频需求更像: + +```text +product-data 生成数据时,用户偏好什么确认流程? +标签大师判断时,哪些边界经常被纠正? +online-mining-v2 查询线上日志时,哪些字段和表更稳定? +某个 Skill 写文件时,模型容易踩什么坑? +``` + +这些经验天然和 Skill 绑定。因此 `skills/.md` 比单一用户记忆更准确。 + +### 5.2 我们需要可审计、可编辑,而不是完全黑盒 + +团队平台里,记忆不能只存在模型或向量库内部。用户需要能看到: + +- 记住了什么。 +- 为什么下一次会注入。 +- 哪里可以手动修改。 +- 哪些记忆是用户级,哪些是 Skill 级。 + +Markdown 文件在这点上比纯向量库更直接。 + +### 5.3 主链路不能被记忆整理拖慢 + +数据生成、线上挖掘、标签判断本身就是长任务。记忆整理如果同步放在主链路里,会增加延迟和失败面。 + +当前设计是: + +```text +主链路:只读取已有记忆 + 入队事件 +后台:异步整理、合并、失败重试/记录 +``` + +这和团队生产工具的稳定性要求更匹配。 + +### 5.4 Skill 作用域注入能降低上下文污染 + +如果所有记忆每次都注入,模型会被无关偏好干扰。当前只注入启用 Skill 的记忆: + +```text +启用 product-data -> 注入 product-data 使用记忆 +启用 label-master -> 注入 label-master 使用记忆 +未启用某 Skill -> 不注入该 Skill 记忆 +``` + +这使记忆更像“能力使用手册的增量补丁”,而不是一坨全局上下文。 + +## 6. 当前不足和后续方向 + +### 6.1 缺少语义召回 + +当前 Skill 记忆是按 Skill 文件整体注入,不做向量检索。如果某个 Skill 记忆变得很长,可能需要: + +- 按章节拆分。 +- 引入轻量 embedding 检索。 +- 只注入和当前 query 相关的片段。 + +### 6.2 缺少结构化 schema + +Markdown 易编辑,但不方便做强约束。后续可以让 Skill 记忆同时存在: + +```text +human.md +structured.json +``` + +其中 Markdown 给人看,JSON 给程序做筛选和校验。 + +### 6.3 缺少记忆质量评估 + +目前能看到队列状态,但还没有系统评估: + +- 哪些记忆被注入。 +- 注入后是否减少纠错。 +- 哪些记忆过期。 +- 哪些 Skill 记忆导致误导。 + +后续可以把 memory revision 与 session outcome 关联起来。 + +### 6.4 缺少跨用户团队记忆 + +当前是账号级记忆。团队共性经验仍主要沉淀在 Skill 本体里。未来可以区分: + +```text +个人 Skill 记忆 + 某个用户自己的偏好和使用习惯。 + +团队 Skill 记忆 + 多人使用后沉淀的稳定经验,经 review 后合入 Skill。 +``` + +这样可以形成从“个人经验”到“团队 Skill 知识”的晋升路径。 + +## 7. 建议的技术路线 + +短期保持当前架构: + +```text +Markdown 可编辑记忆 +SQLite 异步队列 +按 Skill 注入 +UI 可查看可修改 +后台可观测队列 +``` + +中期增强: + +```text +记忆片段化 +记忆注入日志 +过期/冲突检测 +记忆质量指标 +``` + +长期可选: + +```text +向量检索:解决 Skill 记忆膨胀后的相关片段召回。 +图谱记忆:解决用户、Skill、标签、数据集、错误类型之间的关系分析。 +团队记忆晋升:把多用户共性 Skill 经验 review 后写回 Git Skill。 +``` + +## 8. 资料来源 + +- OpenAI Help:ChatGPT Memory FAQ + https://help.openai.com/en/articles/8590148-memory-faq +- OpenAI Agents SDK:Sessions + https://openai.github.io/openai-agents-python/sessions/ +- Anthropic Claude Code:Memory + https://docs.anthropic.com/en/docs/claude-code/memory +- LangChain / LangGraph:Memory concepts + https://docs.langchain.com/oss/python/concepts/memory +- Mem0 documentation + https://docs.mem0.ai/ +- Letta documentation + https://docs.letta.com/ +- Zep / Graphiti documentation + https://help.getzep.com/ +- CrewAI Memory concepts + https://docs.crewai.com/concepts/memory +- Microsoft AutoGen AgentChat Memory + https://microsoft.github.io/autogen/dev/user-guide/agentchat-user-guide/memory.html +- ZK Data Agent 当前实现 + `src/personal_memory.py`、`docs/technical-architecture/05-workspace-memory-observability.md` diff --git a/docs/technical-architecture/11-workspace-runtime.md b/docs/technical-architecture/11-workspace-runtime.md new file mode 100644 index 0000000..9e599a2 --- /dev/null +++ b/docs/technical-architecture/11-workspace-runtime.md @@ -0,0 +1,663 @@ +# Workspace Runtime 设计稿 + +## 背景 + +当前项目已经有了平台账号、会话目录、Jupyter 远程工作区、Skill/Tools 和文件产物管理,但这些能力还没有被一个统一的“执行环境”概念串起来。 + +现在的问题不是单纯缺少登录账号,而是需要回答: + +```text +谁在使用 Agent + -> 当前会话绑定到哪个工作区 + -> 工具以什么身份、在什么目录、用什么权限执行 + -> 产物在哪里保存、展示和下载 +``` + +因此,账户体系升级不应该只看账号密码,而应该引入 `Workspace Runtime` 作为平台账号和工具执行之间的核心抽象。 + +## 核心结论 + +账户体系分两层: + +```text +平台账号 Account + 负责登录、角色、会话、Skill 配置、模型配置、记忆和集成状态。 + +工作区运行时 Workspace Runtime + 负责执行身份、工作目录、文件读写、Python 环境、远程连接和进程管理。 +``` + +平台账号不直接等价于 Linux 账号,也不直接等价于 Jupyter 账号。平台账号可以绑定不同类型的 runtime。 + +## 目标 + +1. 统一本机工作区、Linux 子账户工作区、Jupyter 工作区和未来 SSH 工作区。 +2. 让 Tool handler 不关心执行位置,只面向统一 runtime 执行。 +3. 明确权限来源,避免把远程工作区误认为平台托管沙盒。 +4. 让每个 session 的输入、输出、scratchpad、Python 环境和文件下载有稳定归属。 +5. 为后续多用户、资源限制、审计、团队空间和远程执行打基础。 + +## 非目标 + +1. 不在第一阶段实现完整企业 SSO。 +2. 不把所有账号体系直接迁移到 Linux PAM。 +3. 不强制所有远程工作区都变成平台托管沙盒。 +4. 不要求 Skill 感知 runtime 的具体实现细节。 + +## 对象模型 + +### Account + +平台账号是 Web 产品层的身份。 + +```text +Account + id + username + display_name + role + status + created_at + updated_at +``` + +职责: + +- 登录和会话 token。 +- 模型选择。 +- Skill 启用状态。 +- 用户记忆。 +- 第三方集成状态。 +- 默认 workspace runtime 策略。 + +### Session + +Session 是一次 Agent 对话任务。 + +```text +Session + id + account_id + runtime_id + title + status + created_at + updated_at +``` + +职责: + +- 保存对话历史。 +- 绑定一个 runtime。 +- 保存工具调用、活动步骤和最终结果。 +- 关联输入文件和输出 artifact。 + +Session 一旦绑定远程 runtime,刷新页面后也应该恢复到同一个 runtime。 + +### Workspace Runtime + +Workspace Runtime 是工具执行的真实环境。 + +```text +WorkspaceRuntime + id + account_id + session_id + type + root + permissions_source + status + created_at + updated_at +``` + +`type` 可以是: + +```text +local_process +local_linux_user +remote_jupyter +remote_ssh +``` + +职责: + +- 决定 bash/python/file 工具在哪里执行。 +- 决定输入输出文件在哪里。 +- 决定 Python 环境在哪里。 +- 决定进程如何启动、停止和清理。 +- 决定文件如何展示、下载和转在线文档。 + +### Artifact + +Artifact 是输入和输出文件的统一抽象。 + +```text +Artifact + id + account_id + session_id + runtime_id + kind: input | output | scratchpad + uri + name + size + mime + created_at +``` + +`uri` 可以是: + +```text +file:///home//zk-agent/sessions//output/a.jsonl +jupyter:///root/zk_agent_workspaces//output/a.jsonl +ssh:///home/user/zk_agent_workspaces//output/a.jsonl +``` + +文件列表只需要展示 metadata。下载或转在线文档时,再通过 runtime 拉取内容。 + +### Executor + +Executor 是 Tool handler 和 Runtime 之间的执行适配层。 + +```text +Executor + run_bash(command, cwd, timeout) + run_python(code_or_file, cwd, timeout) + read_file(path) + write_file(path, content) + list_files(path) + open_file_stream(path) + cancel(run_id) +``` + +Tool handler 不应该自己判断是在本地、Jupyter 还是 SSH。它只调用当前 session 的 executor。 + +## Runtime 类型 + +### local_process + +当前已有的默认模式。工具在服务进程所在机器上执行,目录由平台约定。 + +```text +.port_sessions/accounts//sessions// +``` + +适合: + +- 本地开发。 +- 单用户调试。 +- 早期兼容。 + +问题: + +- 多用户隔离主要靠代码路径约束。 +- 工具进程和平台服务权限一致,风险较高。 + +### local_linux_user + +平台托管的标准多用户工作区。 + +```text +平台账号: banisherwy +Linux runtime user: banisherwy +workspace root: /home/banisherwy/zk-agent/sessions/ +``` + +服务进程可以是 root,工具进程切换到普通 Linux 用户执行。 + +```text +root backend + -> runuser -u banisherwy -- +``` + +职责分工: + +```text +root 服务 + 创建 runtime 用户 + 初始化 workspace + 设置 owner 和权限 + 启停进程 + 管理平台账号和 session + +普通 Linux 用户 + 执行 bash/python + 拥有自己的 workspace + 拥有自己的 Python 虚拟环境 + 只能写自己的目录 +``` + +推荐目录: + +```text +/home//zk-agent/ + sessions/ + / + input/ + output/ + scratchpad/ + session.json + python/ + .venv/ + memory/ + integrations/ +``` + +推荐约定: + +```text +平台用户名 = Linux 用户名 +平台密码 = Linux 用户密码 +Linux 用户允许 SSH 登录 +``` + +这样用户体验更直接: + +- 在平台创建账号时,同步创建同名 Linux 用户。 +- 用户可以使用同一套账号密码登录 Web 平台和 SSH。 +- Agent 工具执行时也使用同一个 Linux 用户身份。 +- 文件 owner、进程 owner、SSH 登录用户和平台用户名一致,便于排查和审计。 + +但两者在架构语义上仍然保留分层: + +```text +平台账号体系 + 登录、角色、session、Skill、模型配置。 + +Linux 用户体系 + 执行隔离、文件权限、进程权限、资源限制。 +``` + +也就是说,账号名和密码保持一致,但平台仍然保留自己的登录态、session、角色和配置管理。Linux 账号负责机器级登录和执行权限。 + +需要注意: + +- 用户名必须同时满足平台账号规范和 Linux 用户名规范。 +- 修改平台密码时必须同步修改 Linux 密码。 +- 禁用平台账号时,也应该禁用 Linux 登录或锁定 Linux 用户。 +- 删除平台账号时,需要明确是否保留 `/home//zk-agent/` 数据。 +- root 服务创建用户和改密码时必须走受控 helper,不能把用户输入拼成 shell 命令。 + +### remote_jupyter + +用户授权的远程工作区。 + +语义是: + +```text +用户把自己已有权限的 Jupyter 环境接入平台。 +平台代替用户在这个环境里执行。 +``` + +这不是平台托管沙盒。权限边界来自用户提供的 Jupyter 凭证。 + +```text +Account: banisherwy +Session: xxx +Runtime: remote_jupyter +Root: /root/zk_agent_workspaces/ +Permissions source: Jupyter password/token 对应的远程用户权限 +``` + +平台需要保证: + +- Jupyter 凭证只绑定当前 account/session。 +- 刷新后 runtime 状态可恢复。 +- 文件列表 metadata-only。 +- 下载时通过 Jupyter API 流式拉取。 +- 转在线文档时按需拉取,不默认同步大文件。 +- 用户明确知道 Agent 在远程环境里的权限等同于该 Jupyter 用户。 + +平台不能保证: + +- 远程机器上的文件权限隔离。 +- 远程 Jupyter 用户不是 root。 +- 远程挂载目录的访问范围。 + +短期建议:`remote_jupyter` 先保持当前逻辑,不作为账户体系升级的主战场。 + +当前已经具备: + +- session 级 Jupyter 绑定。 +- 刷新后恢复远程工作区状态。 +- 输出文件 metadata-only 展示。 +- 下载时通过 Jupyter API 流式读取。 +- 转在线文档时按需拉取。 + +因此下一步账户体系升级优先处理本机托管 runtime 和平台账号,不主动重构 Jupyter 执行链路。后续只需要让 Jupyter 工作区在概念上挂到 `WorkspaceRuntime` 模型下。 + +### remote_ssh + +未来可扩展的用户授权远程工作区。 + +语义和 remote_jupyter 类似: + +```text +用户提供 SSH 连接能力。 +平台代替用户在远程机器上执行。 +权限边界来自 SSH 凭证对应的远程用户。 +``` + +remote_ssh 更适合: + +- 远程机器没有 Jupyter。 +- 需要更完整 shell 能力。 +- 需要使用远程开发机的挂载盘、GPU、模型目录。 + +但它也更复杂: + +- SSH 凭证管理。 +- 长连接和心跳。 +- relay / OTP / 扫码登录。 +- 文件传输和断线恢复。 +- 进程树管理。 + +因此优先级应低于 `local_linux_user` 和已有 `remote_jupyter`。 + +## 权限边界 + +需要在 UI 和文档中明确区分两类工作区: + +```text +平台托管工作区 + 平台负责权限隔离。 + 典型类型: local_linux_user。 + +用户授权工作区 + 用户提供凭证。 + 平台不创建权限边界,只复用用户已有权限。 + 典型类型: remote_jupyter, remote_ssh。 +``` + +UI 可以显示: + +```text +当前工作区:Jupyter 远程工作区 +权限来源:用户提供的 Jupyter 凭证 +Agent 权限:等同于该远程环境当前登录用户 +``` + +或者: + +```text +当前工作区:平台托管工作区 +执行身份:banisherwy +Agent 权限:普通 Linux 用户权限 +``` + +## Tool 调用关系 + +目标关系: + +```text +Agent Loop + -> Tool handler + -> RuntimeResolver(session_id) + -> Executor + -> local process / linux user / jupyter / ssh +``` + +工具不应该散落处理路径和远程协议。 + +例如: + +```text +python_exec + -> executor.run_python(...) + +write_file + -> executor.write_file(...) + +download_artifact + -> executor.open_file_stream(...) +``` + +这样后续新增 runtime 时,尽量只新增 executor,不重写每个工具。 + +## 文件策略 + +### 输入文件 + +输入文件应该同步到当前 runtime 的 `input/`。 + +```text +local_linux_user + 上传文件 -> /home//zk-agent/sessions//input/ + +remote_jupyter + 上传文件 -> 通过 Jupyter API 写入 /root/zk_agent_workspaces//input/ +``` + +### 输出文件 + +输出文件默认放到当前 runtime 的 `output/`。 + +```text +output/ + records.jsonl + report.md + samples.csv +``` + +对远程 runtime,平台只保存 metadata。 + +```text +name +size +mtime +uri +runtime_id +``` + +点击下载时再流式读取。点击转在线文档时再按需拉取,并设置大小限制。 + +## Python 环境策略 + +每个 runtime 应有自己的 Python 环境。 + +```text +local_linux_user + /home//zk-agent/python/.venv + +remote_jupyter + /root/zk_agent_workspaces/.zk-agent-python/.venv +``` + +初始化时只做最小准备: + +- 创建 venv。 +- 配置 pip 源。 +- 不预装大量包。 + +缺包时由 Agent 根据任务安装,安装也发生在当前 runtime 内。 + +## 进程管理 + +每个工具执行必须有 run id 和 process group。 + +```text +run_id +account_id +session_id +runtime_id +executor_pid 或 remote_execution_id +status +started_at +updated_at +``` + +停止任务时: + +- local_process:杀本地进程组。 +- local_linux_user:杀对应 runtime 用户下该 run 的进程组。 +- remote_jupyter:中断 kernel 或关闭对应执行任务。 +- remote_ssh:杀远程进程组。 + +不能只停止 Web 请求,否则会出现“前端以为停了,后台 Python 还在跑”的问题。 + +## 持久化建议 + +建议把当前 JSON 账号体系逐步迁到 SQLite。 + +第一阶段可新增这些表: + +```text +accounts + id + username + password_hash + role + status + created_at + updated_at + +account_sessions + token_hash + account_id + created_at + updated_at + expires_at + +workspace_runtimes + id + account_id + session_id + type + root + status + config_json + created_at + updated_at + +artifacts + id + account_id + session_id + runtime_id + kind + uri + name + size + mime + created_at +``` + +敏感信息不要直接明文落库。Jupyter 密码、SSH key、token 至少需要加密或放入受控 secret store。 + +## 与现有实现的关系 + +当前已有能力可以映射到新模型: + +```text +frontend/app/lib/claw-auth.ts + Account 登录态原型。 + +.port_sessions/accounts/ + local_process 模式下的 account workspace。 + +backend/api/server.py::account_paths + Runtime path resolver 的雏形。 + +src/jupyter_runtime.py + remote_jupyter executor 的雏形。 + +RunManager / RunStateStore + run id、活动状态、停止任务的雏形。 + +frontend 文件面板 + Artifact list/download 的雏形。 +``` + +所以这不是推翻重来,而是把已有能力抽象成更稳定的边界。 + +## 演进路线 + +### Phase 0:明确概念,不改执行路径 + +- 在代码和文档中引入 Workspace Runtime 术语。 +- 把现有 `.port_sessions/accounts/` 视为 `local_process` runtime。 +- UI 显示当前工作区类型。 +- 对 Jupyter 工作区补充权限提示。 + +### Phase 1:抽象 RuntimeResolver 和 Executor + +- 新增 `RuntimeResolver`,根据 account/session 找当前 runtime。 +- 新增统一 `Executor` 接口。 +- 先把 `python_exec`、`bash`、文件工具迁到 executor。 +- 保持现有 local 和 Jupyter 行为不变。 + +### Phase 2:账号存储升级 + +- 把 `users.json` 和 `auth_sessions.json` 迁到 SQLite。 +- 增加 `account_id`、`role`、`status`、`expires_at`。 +- 增加 session token 清理。 +- 管理后台去掉 `admin/admin` 和默认 `123456`。 + +### Phase 3:local_linux_user runtime + +- root 服务创建与平台账号同名的 Linux 用户。 +- 平台密码同步设置为 Linux 用户密码。 +- Linux 用户允许 SSH 登录。 +- 初始化 `/home//zk-agent/`。 +- 工具执行切到普通 Linux 用户。 +- Python venv、session、output 全部进入用户 home。 +- 停止任务时按 process group 清理。 + +### Phase 4:资源限制和审计 + +- ulimit / cgroup。 +- 每账号磁盘 quota。 +- 工具执行审计。 +- 大文件下载限流。 +- session/output 清理策略。 + +### Phase 5:remote_ssh runtime + +- 在 remote_jupyter 稳定后再考虑。 +- 重点解决认证、relay、长连接、文件传输和远程进程清理。 + +## 关键待决问题 + +1. 平台账号是否允许用户自注册,还是只允许管理员创建? +2. 用户自注册时,是否允许自动创建同名 Linux 用户? +3. 删除账号时,是否删除 Linux 用户,是否保留 home 目录? +4. 本机平台托管 workspace 是否统一迁到 `/home//zk-agent/`? +5. Jupyter 凭证如何加密保存? +6. 远程 workspace 产物保留多久? +7. 大文件下载、在线文档转换和文件预览的大小限制是多少? +8. 是否需要团队空间:一个 workspace 被多个账号共享? + +## 推荐决策 + +短期建议: + +```text +保留平台账号体系。 +引入 Workspace Runtime 抽象。 +继续稳定 remote_jupyter。 +账号存储从 JSON 迁到 SQLite。 +开始设计 local_linux_user,但不要立即替换所有执行路径。 +``` + +中期建议: + +```text +服务可以 root 运行。 +平台账号创建时同步创建同名普通 Linux 用户。 +平台密码和 Linux 密码保持一致。 +Linux 用户允许 SSH 登录。 +工具执行统一通过 runtime executor。 +本机默认工作区逐步迁到 /home//zk-agent。 +``` + +长期建议: + +```text +平台账号负责产品身份。 +Workspace Runtime 负责执行环境。 +Artifact 负责跨 runtime 文件抽象。 +Executor 负责工具执行适配。 +``` + +这样账户体系、Linux 子账户、Jupyter/SSH 远程工作区、文件下载、Python 环境和工具执行可以合到一个统一设计里,而不是继续各自生长。 diff --git a/docs/technical-architecture/README.md b/docs/technical-architecture/README.md new file mode 100644 index 0000000..8e713bd --- /dev/null +++ b/docs/technical-architecture/README.md @@ -0,0 +1,60 @@ +# 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) +10. [Agent 记忆机制调研与对比](10-memory-research.md) + +## 一句话定位 + +ZK Data Agent 的基座是一个 Web 化、多用户、可观测的 Agent runtime。 +业务能力通过 `skills//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` 页面。 +- 页面表达、图形素材、演示脚本不写在这里;这里只保留技术事实和设计边界。 diff --git a/docs/technical-architecture/assets/00-zk-data-agent-architecture.png b/docs/technical-architecture/assets/00-zk-data-agent-architecture.png new file mode 100644 index 0000000..e7c517d Binary files /dev/null and b/docs/technical-architecture/assets/00-zk-data-agent-architecture.png differ diff --git a/docs/technical-architecture/assets/01-base-runtime.png b/docs/technical-architecture/assets/01-base-runtime.png new file mode 100644 index 0000000..51497a4 Binary files /dev/null and b/docs/technical-architecture/assets/01-base-runtime.png differ diff --git a/docs/technical-architecture/assets/02-agent-loop.png b/docs/technical-architecture/assets/02-agent-loop.png new file mode 100644 index 0000000..6f5e991 Binary files /dev/null and b/docs/technical-architecture/assets/02-agent-loop.png differ diff --git a/docs/technical-architecture/assets/03-tools.png b/docs/technical-architecture/assets/03-tools.png new file mode 100644 index 0000000..e15b8b8 Binary files /dev/null and b/docs/technical-architecture/assets/03-tools.png differ diff --git a/docs/technical-architecture/assets/04-skills.png b/docs/technical-architecture/assets/04-skills.png new file mode 100644 index 0000000..51e77cc Binary files /dev/null and b/docs/technical-architecture/assets/04-skills.png differ diff --git a/docs/technical-architecture/assets/05-workspace-memory-observability.png b/docs/technical-architecture/assets/05-workspace-memory-observability.png new file mode 100644 index 0000000..99a376f Binary files /dev/null and b/docs/technical-architecture/assets/05-workspace-memory-observability.png differ diff --git a/docs/technical-architecture/assets/06-product-data.png b/docs/technical-architecture/assets/06-product-data.png new file mode 100644 index 0000000..e737a2a Binary files /dev/null and b/docs/technical-architecture/assets/06-product-data.png differ diff --git a/docs/technical-architecture/assets/07-online-mining-v2.png b/docs/technical-architecture/assets/07-online-mining-v2.png new file mode 100644 index 0000000..e1336cb Binary files /dev/null and b/docs/technical-architecture/assets/07-online-mining-v2.png differ diff --git a/docs/technical-architecture/assets/08-label-master.png b/docs/technical-architecture/assets/08-label-master.png new file mode 100644 index 0000000..db0ac87 Binary files /dev/null and b/docs/technical-architecture/assets/08-label-master.png differ diff --git a/docs/technical-architecture/assets/09-external-skills.png b/docs/technical-architecture/assets/09-external-skills.png new file mode 100644 index 0000000..f8674f3 Binary files /dev/null and b/docs/technical-architecture/assets/09-external-skills.png differ diff --git a/docs/technical-architecture/assets_black/01-base-runtime.png b/docs/technical-architecture/assets_black/01-base-runtime.png new file mode 100644 index 0000000..c400aa5 Binary files /dev/null and b/docs/technical-architecture/assets_black/01-base-runtime.png differ diff --git a/docs/technical-architecture/assets_black/02-agent-loop.png b/docs/technical-architecture/assets_black/02-agent-loop.png new file mode 100644 index 0000000..d420775 Binary files /dev/null and b/docs/technical-architecture/assets_black/02-agent-loop.png differ diff --git a/docs/technical-architecture/assets_black/03-tools.png b/docs/technical-architecture/assets_black/03-tools.png new file mode 100644 index 0000000..9f6672c Binary files /dev/null and b/docs/technical-architecture/assets_black/03-tools.png differ diff --git a/docs/technical-architecture/assets_black/04-skills.png b/docs/technical-architecture/assets_black/04-skills.png new file mode 100644 index 0000000..757bb9b Binary files /dev/null and b/docs/technical-architecture/assets_black/04-skills.png differ diff --git a/docs/technical-architecture/assets_black/05-workspace-memory-observability.png b/docs/technical-architecture/assets_black/05-workspace-memory-observability.png new file mode 100644 index 0000000..5eb03d9 Binary files /dev/null and b/docs/technical-architecture/assets_black/05-workspace-memory-observability.png differ diff --git a/docs/technical-architecture/assets_black/06-product-data.png b/docs/technical-architecture/assets_black/06-product-data.png new file mode 100644 index 0000000..562624b Binary files /dev/null and b/docs/technical-architecture/assets_black/06-product-data.png differ diff --git a/docs/technical-architecture/assets_black/07-online-mining-v2.png b/docs/technical-architecture/assets_black/07-online-mining-v2.png new file mode 100644 index 0000000..e0c4e39 Binary files /dev/null and b/docs/technical-architecture/assets_black/07-online-mining-v2.png differ diff --git a/docs/technical-architecture/assets_black/08-label-master.png b/docs/technical-architecture/assets_black/08-label-master.png new file mode 100644 index 0000000..609131c Binary files /dev/null and b/docs/technical-architecture/assets_black/08-label-master.png differ diff --git a/docs/technical-architecture/assets_black/09-external-skills.png b/docs/technical-architecture/assets_black/09-external-skills.png new file mode 100644 index 0000000..52a6742 Binary files /dev/null and b/docs/technical-architecture/assets_black/09-external-skills.png differ diff --git a/frontend/app/app/api/chat/route.ts b/frontend/app/app/api/chat/route.ts index 927743b..fca7a2e 100644 --- a/frontend/app/app/api/chat/route.ts +++ b/frontend/app/app/api/chat/route.ts @@ -11,6 +11,7 @@ import { accountSessionOutputRoot, accountSessionRoot, accountSessionScratchpadRoot, + chownAccountPath, getCurrentAccount, } from "@/lib/claw-auth"; @@ -334,14 +335,16 @@ async function getLastUserText( } async function ensureSessionDirectories(accountId: string, sessionId: string) { + const sessionRoot = accountSessionRoot(accountId, sessionId); await Promise.all([ - mkdir(accountSessionRoot(accountId, sessionId), { recursive: true }), + mkdir(sessionRoot, { recursive: true }), mkdir(accountSessionInputRoot(accountId, sessionId), { recursive: true }), mkdir(accountSessionOutputRoot(accountId, sessionId), { recursive: true }), mkdir(accountSessionScratchpadRoot(accountId, sessionId), { recursive: true, }), ]); + await chownAccountPath(accountId, sessionRoot, true); } function renderSessionRuntimeContext(accountId: string, sessionId: string) { @@ -394,6 +397,7 @@ async function saveFilePart( await mkdir(uploadDir, { recursive: true }); const filePath = path.join(uploadDir, `${Date.now()}-${filename}`); await writeFile(filePath, bytes); + await chownAccountPath(accountId, filePath); return [ `[文件已上传] ${filename}`, diff --git a/frontend/app/app/doc/[slug]/page.tsx b/frontend/app/app/doc/[slug]/page.tsx new file mode 100644 index 0000000..b660502 --- /dev/null +++ b/frontend/app/app/doc/[slug]/page.tsx @@ -0,0 +1,373 @@ +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", + }, + "10-memory-research": { + title: "Agent 记忆机制调研与对比", + file: "10-memory-research.md", + image: null, + }, +} 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({token.slice(1, -1)}); + } else if (token.startsWith("**")) { + nodes.push({token.slice(2, -2)}); + } else { + const href = match[3] ?? ""; + const isExternal = /^https?:\/\//.test(href); + nodes.push( + + {match[2]} + , + ); + } + + 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

{parseInline(text)}

; + if (depth === 2) return

{parseInline(text)}

; + if (depth === 3) return

{parseInline(text)}

; + if (depth === 4) return

{parseInline(text)}

; + if (depth === 5) return
{parseInline(text)}
; + return
{parseInline(text)}
; +} + +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( +
+
{language}
+
+						{codeLines.join("\n")}
+					
+
, + ); + 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[1]}, + ); + 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( + + + + {headers.map((cell) => ( + + ))} + + + + {rows.map((row) => ( + + {row.map((cell) => ( + + ))} + + ))} + +
{parseInline(cell)}
{parseInline(cell)}
, + ); + 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( +
    + {items.map((item) => ( +
  • {parseInline(item)}
  • + ))} +
, + ); + 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( +
    + {items.map((item) => ( +
  1. {parseInline(item)}
  2. + ))} +
, + ); + 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( +
+ {parseInline(quoteLines.join(" "))} +
, + ); + 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( +

{parseInline(paragraphLines.join(" "))}

, + ); + } + + return blocks; +} + +export function generateStaticParams() { + return Object.keys(technicalDocs).map((slug) => ({ slug })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug: string }>; +}): Promise { + 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 ( +
+
+ +
+

技术文档

+

{doc.title}

+ + 返回架构总览 + +
+
+ +
+
+ {renderMarkdown(markdown)} +
+
+
+ ); +} diff --git a/frontend/app/app/doc/page.tsx b/frontend/app/app/doc/page.tsx index a31af16..cb07c55 100644 --- a/frontend/app/app/doc/page.tsx +++ b/frontend/app/app/doc/page.tsx @@ -1,589 +1,302 @@ -"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_exec;write_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//SKILL.md", - purpose: "定义 Agent 如何识别任务、何时澄清、何时 review、何时调用脚本。", - agentSees: "流程协议、工具优先级、产物约定、人类确认边界。", - maintainerDoes: "把经验写成可执行步骤,而不是把所有业务细节塞成一大段 prompt。", - }, - knowledge: { - name: "knowledge/", - path: "skills//knowledge/", - purpose: "放业务定义、标签边界、字段说明、判断准则。", - agentSees: "按需读取具体知识文件,用于分析和生成。", - maintainerDoes: "把经常变的业务规则放这里,降低改 SKILL.md 的频率。", - }, - scripts: { - name: "scripts/", - path: "skills//scripts/", - purpose: "放确定性能力:校验、格式转换、导出、数据清洗。", - agentSees: "通过 python_exec 执行脚本,不再现场手写复杂转换逻辑。", - maintainerDoes: "把易错代码沉淀成可测试脚本,输入输出写清楚。", - }, - examples: { - name: "examples/", - path: "skills//examples/", - purpose: "放典型输入输出,帮助 Agent 对齐格式和边界。", - agentSees: "少量高质量样例,比长篇抽象说明更稳。", - maintainerDoes: "新增失败 case 的最小复现,作为回归材料。", - }, - readme: { - name: "README.md", - path: "skills//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 exampleSessions = [ + { + title: "product-data", + description: "数据生成链路:目标抽取、计划确认、分批生成、canonical records 和训练/评测格式导出。", + links: [ + "http://10.189.47.6/session/__LOCALID_ZgwXdfP", + "http://10.189.47.6/session/__LOCALID_QOecDTu", + ], + }, + { + title: "标签大师", + description: "标签知识判断链路:复杂度、多指令、自动任务、function/tag 输出形态和边界知识引用。", + links: [ + "http://10.189.47.6/session/__LOCALID_ji5lCfb", + "http://10.189.47.6/session/__LOCALID_Z22r3pn", + "http://10.189.47.6/session/__LOCALID_jsu7aIX", + "http://10.189.47.6/session/__LOCALID_WjULYPt", + "http://10.189.47.6/session/__LOCALID_YJyPnF7", + ], + }, + { + title: "标签大师 + online-mining", + description: "组合链路:先用线上日志定位样本,再结合标签知识做判断、整理和格式转换。", + links: ["http://10.189.47.6/session/__LOCALID_OLKxKnf"], + }, + { + title: "online-mining", + description: "线上挖掘链路:查询策略、ELK 字段探索、候选样本抽样 review 和标准数据沉淀。", + links: [ + "http://10.189.47.6/session/__LOCALID_BnxMLqn", + "http://10.189.47.6/session/__LOCALID_xZRilR5", + "http://10.189.47.6/session/__LOCALID_CBy2JB5", + ], + }, ]; -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 ; - if (value === "partial") return ; - return ; -} - -function ProductComparison() { - const [selected, setSelected] = useState("zk"); - const detail = products[selected]; - const productIds: ProductId[] = ["chatgpt", "cursor", "claude", "zk"]; - +function SectionBlock({ section }: { section: Section }) { return ( -
-
-

先回答最尖锐的问题

-

为什么不用 Claude Code / ChatGPT / Cursor 就够了?

-

- 不是因为我们模型更强,而是因为我们补的是团队业务流程平台:多人入口、共享 Skill、数据源、产物、记忆和管理。 -

+
+
+ {section.no} +
+

{section.title}

+

{section.summary}

+
-
-
- {productIds.map((id) => ( - + {section.image ? ( + + {section.title} + + ) : null} +
+
+

关键机制

+ {section.points.map((point) => ( +

+ {point} +

))}
-
-

当前选中

-

{detail.name}

-
-
- 最适合 -

{detail.bestFor}

-
-
- 边界 -

{detail.limits}

-
-
-
- {detail.tech.map((item) => ( - {item} - ))} -
-
-
-
-
- 能力 - {productIds.map((id) => {products[id].name})} -
- {comparisonRows.map((row) => ( -
- {row.label} - {productIds.map((id) => ( -
- -
- ))} -
- ))} -
-
- ); -} - -function RoleSwitcher() { - const [selected, setSelected] = useState("user"); - const role = roles[selected]; - - return ( -
-
-

融入工作的方式

-

同一个平台,三类人看到的是三条不同路径

-
-
-
- {(Object.keys(roles) as RoleId[]).map((id) => ( - +
+

代码入口

+ {section.code.map((item) => ( + {item} ))} + {section.doc ? ( + + 查看详细文档 + + ) : null}
-
-

{role.title}

-
{role.question}
-

{role.answer}

-
- {role.workflow.map((step) => ( - {step} - ))} -
+
+

设计取舍

+

{section.decision}

-
-

平台证据

- {role.proof.map((item) => ( -
- - {item} -
- ))} -
-
-
- ); -} - -function ObjectiveBoard() { - return ( -
-
-

这页怎么讲清楚系统

-

每个前端模块都必须服务一个表达目标

-
-
- {objectives.map((item, index) => ( -
- {String(index + 1).padStart(2, "0")} -

{item.goal}

-

{item.frontend}

- {item.message} -
- ))} -
-
- ); -} - -function MemoryAtlas() { - const [selected, setSelected] = useState("ours"); - const item = memoryPatterns[selected]; - const order: MemoryId[] = ["chatgpt", "letta", "mem0", "rag", "ours"]; - - return ( -
-
-

记忆方案谱系

-

我们不是闭门造车:先看主流方案,再看为什么这样落地

-

- 记忆不是一个功能开关。我们参考了显式保存、历史参考、状态化 Agent、归档检索、托管记忆层等方案,最后按团队 Skill 场景做了轻量实现。 -

-
-
-
- {order.map((id, index) => ( - - ))} -
-
- -

{item.name}

-
-
它怎么做

{item.source}

-
我们借鉴

{item.borrowed}

-
为什么不够

{item.notEnough}

-
我们的取舍

{item.ourChoice}

-
-
-
-
-
-

当前实现

-

用户记忆 + Skill 记忆 + 异步队列

-
-
- {memoryPipeline.map((step, index) => ( -
- {String(index + 1).padStart(2, "0")} - {step.title} -

{step.text}

- {step.code} -
- ))} -
-
-
- ); -} - -function SkillWorkbench() { - const [selected, setSelected] = useState("skillmd"); - const part = skills[selected]; - const order: SkillPartId[] = ["skillmd", "knowledge", "scripts", "examples", "readme", "update"]; - - return ( -
-
-

Skill 怎么开发,怎么共享,怎么更新

-

把一个人的经验打包成团队能力,不靠口口相传

-
-
-
- {order.map((id) => ( - - ))} -
-
- {part.path} -

{part.name}

-
-
解决什么

{part.purpose}

-
Agent 看到什么

{part.agentSees}

-
维护者做什么

{part.maintainerDoes}

-
-
-
-

一个 Skill 的提交路径

- {["创建目录", "补 SKILL.md", "沉淀脚本/知识", "本地测试", "提交 Git", "页面点更新 Skill"].map((step, index) => ( -
{index + 1}{step}
- ))} -
-
-
- ); -} - -function TaskFlowMap() { - return ( -
-
-

已经能进入工作的场景

-

不是展示功能点,而是展示从输入到产物的链路

-
-
- {taskFlows.map((flow) => ( -
-

{flow.name}

-
Skill{flow.skill}
-
输入

{flow.input}

-
Review 点

{flow.review}

-
- {flow.outputs.map((output) => {output})} -
-
- ))} -
-
- ); -} - -function RuntimeTrace() { - return ( -
-
-

运行时链路

-

从用户按下发送,到文件、活动区和记忆更新

-
-
- {runtimeTrace.map((step, index) => ( -
- {String(index + 1).padStart(2, "0")} - {step.stage} -

{step.text}

- {step.path} -
- ))}
); @@ -591,64 +304,150 @@ function RuntimeTrace() { export default function DocPage() { return ( -
-
-