跳转至

架构

概览

┌─────────────────────────────────────────────────────────────┐
│                        CLI 层                                │
│  cli.py ─── 参数解析、模式分发                                │
│  repl.py ── 交互式 REPL + 斜杠命令                           │
│  ui.py ──── 基于 Rich 的终端输出                              │
├─────────────────────────────────────────────────────────────┤
│                       代理层                                 │
│  agent.py ── 代理循环(LLM → 工具 → LLM → ...)              │
│  client.py ─ 智谱 API 客户端                                  │
│  models.py ─ 模型注册表(GLM-5、GLM-4-flash、GLM-4-air)      │
├─────────────────────────────────────────────────────────────┤
│                       工具层                                 │
│  tools/base.py ──── BaseTool 抽象基类 + Registrable 协议      │
│  tools/__init__.py ─ ToolRegistry + 工厂函数                  │
│  tools/file_read.py、file_write.py、file_list.py              │
│  tools/ocr.py、shell.py、web_fetch.py、skill_create.py       │
│  tools/skill_tool.py ── 嵌套代理的 SkillTool 包装器           │
├─────────────────────────────────────────────────────────────┤
│                       技能层                                 │
│  skills/loader.py ── YAML 解析 + 验证                        │
│  skills/__init__.py ─ 发现机制(内置 + 用户目录)              │
│  skills/builtin/*.yaml ── 15 个内置技能配置                   │
├─────────────────────────────────────────────────────────────┤
│                       支持层                                 │
│  config.py ── 平台特定配置(YAML + 环境变量)                  │
│  i18n.py ──── 双语字符串目录 + 语言前导指令                    │
│  errors.py ── 结构化错误类型                                  │
└─────────────────────────────────────────────────────────────┘

双模型架构

zhi 使用两个具有不同性价比特征的模型:

模型 角色 层级 思考模式 使用场景
GLM-5 交互对话 高级 支持 对话、复杂推理
GLM-4-flash 技能执行 经济 不支持 批量任务、文档处理

为什么使用两个模型?

  • 交互对话受益于 GLM-5 更深层的推理和思考模式
  • 技能(总结、翻译、提取)运行确定性工作流,GLM-4-flash 足以胜任
  • GLM-4-flash 的成本约为 GLM-5 的 10%,使批量操作具有成本效益
  • 用户可以通过 /model 按会话覆盖,或在 YAML 配置中按技能覆盖

代理循环

zhi 的核心是 agent.py 中在 LLM 和工具之间迭代的代理循环:

用户输入 → Context → LLM(带工具 schema)
                    有工具调用?
                    │ 是           │ 否
                    ▼              ▼
              执行工具         返回响应
              追加结果
              循环 ←──────┘

Context

Context 数据类保存单次代理运行的所有状态:

@dataclass
class Context:
    config: Any              # ZhiConfig 实例
    client: ClientLike       # 智谱 API 客户端
    model: str               # 当前活跃模型
    tools: dict[str, ToolLike]  # 可用工具
    tool_schemas: list[dict]    # OpenAI 格式函数 schema
    permission_mode: PermissionMode  # approve 或 auto
    conversation: list[dict]        # 消息历史
    session_tokens: int = 0         # 累计 token 数
    max_turns: int = 30             # 轮次限制
    thinking_enabled: bool = True   # 扩展推理
    # UI 集成回调
    on_stream, on_thinking, on_tool_start, on_tool_end,
    on_permission, on_waiting

循环行为

  1. 将对话 + 工具 schema 发送给 LLM
  2. 如果响应包含工具调用:
  3. 检查权限(approve 模式下的风险工具)
  4. 执行每个工具,输出上限 50KB
  5. 将结果追加到对话
  6. 回到步骤 1
  7. 如果响应为纯文本,返回(代理完成)
  8. 如果达到 max_turns,返回 None

权限检查

工具有风险?
  │ 否 → 立即执行
  │ 是
模式为 approve?
  │ 否(auto)→ 立即执行(shell 除外)
  │ 是
调用 on_permission 回调 → 用户批准? → 执行
                          用户拒绝? → 返回"权限被拒绝"

Shell 始终需要确认

shell 工具的 risky = True,代理循环始终检查其权限,无论权限模式如何。这在工具层面强制执行。


工具注册表

BaseTool 抽象基类

所有内置工具继承自 BaseTool

class BaseTool(ABC):
    name: ClassVar[str]           # 唯一标识符
    description: ClassVar[str]    # LLM 看到的描述
    parameters: ClassVar[dict]    # 参数的 JSON Schema
    risky: ClassVar[bool] = False # 是否需要权限?

    @abstractmethod
    def execute(self, **kwargs) -> str: ...

    def to_function_schema(self) -> dict: ...

ToolRegistry

注册表管理工具实例并生成 schema:

方法 说明
register(tool) 添加工具(重名时抛出 ValueError
get(name) 按名称查找工具
list_tools() 返回所有已注册工具
filter_by_names(names) 按名称列表筛选工具子集
to_schemas() 导出所有工具的 OpenAI 格式函数 schema
to_schemas_filtered(names) 导出工具子集的 schema

注册顺序

# 1. 文件类工具(无外部依赖)
registry = create_default_registry()
# → file_read, file_write, file_list, web_fetch

# 2. 需要运行时依赖的工具
registry.register(OcrTool(client=client))
registry.register(ShellTool(permission_callback=...))

# 3. 技能工具(从 YAML 发现)
skills = discover_skills()
register_skill_tools(registry, skills, client)
# → skill_summarize, skill_translate, ...

技能系统

技能配置

技能以 YAML 文件定义,结构如下:

name: summarize
description: 总结文本文件或文档
model: glm-4-flash
system_prompt: |
  你是一个简洁的摘要助手...
tools:
  - file_read
  - file_write
max_turns: 5
input:
  description: 要总结的文本文件
  args:
    - name: file
      type: file
      required: true
output:
  description: Markdown 摘要
  directory: zhi-output

SkillConfig 数据类

@dataclass
class SkillConfig:
    name: str
    description: str
    system_prompt: str
    tools: list[str]
    model: str = "glm-4-flash"
    max_turns: int = 15
    input_args: list[dict] = field(default_factory=list)
    output_description: str = ""
    output_directory: str = "zhi-output"
    source: str = ""  # "builtin" 或 "user"

技能发现

技能从两个目录中发现:

  1. 内置src/zhi/skills/builtin/*.yaml(随包分发)
  2. 用户:用户定义的目录(同名时覆盖内置技能)

损坏的 YAML 文件会被跳过并记录警告。

组合技能

组合技能将其他技能引用为工具。当技能在 tools 中列出 analyze 时,系统将其解析为 skill_analyze 并包装为 SkillTool

# contract-review.yaml
tools:
  - file_read
  - ocr
  - file_write
  - analyze      # → 解析为 skill_analyze
  - compare      # → 解析为 skill_compare
  - proofread    # → 解析为 skill_proofread

递归保护

嵌套技能执行有三个安全机制:

机制 限制 行为
循环检测 不适用 如果技能名称出现在当前调用链中则阻止
深度限制 3 层 超过最大深度时阻止执行
最大轮次 每个技能独立 每个嵌套层有自己的轮次限制

国际化系统

语言前导指令

每个技能提示前都会附加语言前导指令:

IMPORTANT: Always respond in the same language as the input document. If the document or user input is in Chinese, your ENTIRE output -- including all section headers, table headers, column names, labels, and structural elements -- MUST be in Chinese. Never mix languages in your response.

这确保整个技能链(包括嵌套的组合技能)中输出语言的一致性。

字符串目录

界面使用基于键的字符串目录,包含英文和中文翻译:

t("repl.help")           # → 英文或中文帮助文本
t("ui.confirm_rich",     # → "允许 file_write(path)?"
   tool="file_write",
   args="path")

语言解析

显式调用 set_language("zh") → "zh"
                             ↓(如果是 "auto")
ZHI_LANGUAGE 环境变量 → 检查 "zh" 前缀
                             ↓(未设置)
LANG / LC_ALL 环境变量 → 检查 "zh" 前缀
                             ↓(未设置)
默认 → "en"

安全模型

输出隔离

所有文件写入都指向 zhi-output/(可配置)。原始文件永远不会被修改。

检查项 说明
仅限相对路径 绝对路径被拒绝
禁止路径遍历 .. 路径段被阻止
符号链接解析 解析后的路径必须在输出目录内
禁止覆盖 已存在的文件不能被替换

Shell 安全

三级命令分类:

级别 示例 行为
封锁 rm -rf /、fork 炸弹、dd 写设备 始终拒绝,无法确认执行
破坏性 rmmvchmodsed -igit reset --hard 额外警告 + 确认
标准 lswcgrep 标准确认

绕过模式(evalbash -csh -c/bin/rm)也被封锁。

SSRF 防护

web_fetch 工具阻止访问:

  • localhost 和已知元数据端点
  • 私有 IP 范围(RFC 1918)
  • 回环地址和链路本地地址

配置安全

  • 配置文件权限:0o600(仅所有者可访问)
  • API 密钥以纯文本存储(共享系统建议使用 ZHI_API_KEY 环境变量)
  • 敏感输入不记录到 REPL 历史