架构¶
概览¶
┌─────────────────────────────────────────────────────────────┐
│ 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¶
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
循环行为¶
- 将对话 + 工具 schema 发送给 LLM
- 如果响应包含工具调用:
- 检查权限(approve 模式下的风险工具)
- 执行每个工具,输出上限 50KB
- 将结果追加到对话
- 回到步骤 1
- 如果响应为纯文本,返回(代理完成)
- 如果达到
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"
技能发现¶
技能从两个目录中发现:
- 内置:
src/zhi/skills/builtin/*.yaml(随包分发) - 用户:用户定义的目录(同名时覆盖内置技能)
损坏的 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 写设备 |
始终拒绝,无法确认执行 |
| 破坏性 | rm、mv、chmod、sed -i、git reset --hard |
额外警告 + 确认 |
| 标准 | ls、wc、grep |
标准确认 |
绕过模式(eval、bash -c、sh -c、/bin/rm)也被封锁。
SSRF 防护¶
web_fetch 工具阻止访问:
localhost和已知元数据端点- 私有 IP 范围(RFC 1918)
- 回环地址和链路本地地址
配置安全¶
- 配置文件权限:
0o600(仅所有者可访问) - API 密钥以纯文本存储(共享系统建议使用
ZHI_API_KEY环境变量) - 敏感输入不记录到 REPL 历史