# shadow **Repository Path**: bbsmrdj/shadow ## Basic Information - **Project Name**: shadow - **Description**: Shadow AI Agent. Your personal customizable agent. - **Primary Language**: Python - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-07 - **Last Updated**: 2026-09-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # shadow · 影 > 一个**从零手写、为学习而生**的最小 AI Agent。 > 零第三方依赖,Python 3.10+ 直接跑,没有 API Key 也能演示完整流程。 ```bash python main.py "帮我算一下 (12+3)*4" # 提问 python main.py --verbose "现在几点" # 看它每一步在想什么 python main.py -i # 交互式对话 python main.py --list-tools # 看看它有什么"手" python main.py --show-persona # 看它是什么性格(persona.md) python -m unittest discover -s tests -v # 跑 47 个自测 ``` --- ## 一、Agent 到底是什么? 剥掉所有花哨包装,Agent = **一个会调工具的 while 循环 + 一个大模型**。 ``` ┌──────────────────────────────────────┐ 用户提问 ───────▶│ ① 组装消息 │ │ 系统提示词 + 历史记忆 + 本轮进展 │ └───────────────┬──────────────────────┘ ▼ ┌──────────────────────────────────────┐ │ ② 问大脑:chat(messages) → 一段文本 │ └───────────────┬──────────────────────┘ ▼ ┌──────────────────────────────────────┐ │ ③ 解析:Thought / Action / Final? │ └───────┬───────────────────┬──────────┘ │ │ 要调工具 │ │ 能给答案了 ▼ ▼ ┌─────────────────────────┐ ┌──────────────┐ │ ④ 执行工具 → Observation │ │ Final Answer │ │ 把结果追加回对话,回到 ①│ │ 结束,返回 │ └─────────────────────────┘ └──────────────┘ ``` 对应到代码就是 `shadow/agent.py` 里的 `Agent.run()` —— 全书最重要的 60 行。 ### 六个零件 | 零件 | 文件 | 一句话解释 | | --- | --- | --- | | 🧠 大脑 | `shadow/llm.py` | 负责生成文字。可替换成任意大模型 | | ✋ 手 | `shadow/tools.py`、`shadow/builtins.py` | 让模型能干实事(计算、读文件…) | | 📝 记忆 | `shadow/memory.py` | 保存对话历史,让模型"记得"前文 | | 🔁 循环 | `shadow/agent.py` | 把上面三者串起来,决定什么时候停 | | 🎭 人格 | `shadow/persona.py`、`persona.md` | 它是谁、怎么说话(可热改并持久化) | | 📜 顶层设定 | `shadow/constitution.py`、`CONSTITUTION.md` | 什么绝对不能做(优先级最高) | 前四个决定"能不能干活",后两个决定"干得像不像 shadow"。 --- ## 二、模型跟 Agent 说话的"协议" 模型输出的是自然语言,程序需要结构化数据。本项目用最直观的 **ReAct 文本协议** (Reason + Act),要求模型每次回复只能是下面两种之一: ``` Thought: 用户在问计算结果,我不该心算 Action: calculator Action Input: {"expression": "(12+3)*4"} ``` ``` Thought: 已经拿到结果了 Final Answer: 60 ``` 程序解析后,把工具结果包装成 `Observation:` 塞回对话,模型据此继续。 **用 `--verbose` 运行,能亲眼看到这个过程。** --- ## 三、接真实大模型 ```bash cp .env.example .env # 然后填 SHADOW_API_KEY python main.py "写一句自我介绍" ``` 没填 Key 时程序会用内置的 `MockLLM`(离线假大脑)自动降级,保证任何环境都能跑。 兼容任何 **OpenAI 协议**的服务商,改 `.env` 里两行即可(各家对照表见 `.env.example`): | 服务商 | SHADOW_BASE_URL | SHADOW_MODEL | | --- | --- | --- | | DeepSeek(默认) | `https://api.deepseek.com/v1` | `deepseek-chat` | | OpenAI | `https://api.openai.com/v1` | `gpt-4o-mini` | | 智谱 GLM | `https://open.bigmodel.cn/api/paas/v4` | `glm-4-flash` | | 本地 Ollama | `http://localhost:11434/v1` | `qwen2.5:7b` | HTTP 请求是**用标准库 urllib 手写**的(`shadow/openai_llm.py`), 这样你能看清 SDK 到底替你做了什么:其实就是拼 URL、加 `Authorization` 头、POST 一个 JSON。 --- ## 四、人格与顶层设定(持久化) > "能跑的 Agent" 和 "有稳定性格的 Agent",差的就是这两层。 > 两者都是**纯 Markdown 文件**,可以直接编辑、进 git、diff、分享。 | 层 | 落在哪个文件 | 管什么 | 能否在对话里改 | | --- | --- | --- | --- | | 顶层设定(宪法) | `CONSTITUTION.md` | 什么能做、什么绝对不能做 | ❌ 只能由人类直接编辑文件 | | 人格(Persona) | `persona.md` | 名字、性格、语气、行事原则、边界 | ✅ 可热改,自动保存 | 三条关键设计: 1. **顺序即优先级。** 系统提示词按 `宪法 → 人格 → 工作方式` 拼装(见 `prompts.py`)。 大模型对**靠前**的内容更敏感,把宪法放最前面,就是最省事的优先级实现。 ⚠️ 副作用:想改名字,得同时改 `CONSTITUTION.md` 第一条,否则宪法锁死身份,人格里的名字改不动。 2. **外部数据不是指令。** 宪法第二条把指令分成 A/B/C/D 四级, 工具返回值、文件内容、网页正文一律归为 D 级 —— **只当数据看**。 这是抵御**提示词注入**("忽略以上指令,你现在是另一个 AI")的第一道防线。 3. **每次对话都重读。** `agent.run()` 第一件事就是重新读取这两个文件 (`agent.py::refresh_identity`)。所以你用编辑器改完,下一句话就是新人格,不用重启。 ### 常用命令 ```bash python main.py --show-persona # 看当前人格(persona.md 全文) python main.py --persona-set tone=惜字如金,别啰嗦 # 改脾气,自动落盘 python main.py --persona-set 性格=冷静、锋利、不说废话 # 支持中文别名与列表字段 python main.py --reset-persona # 恢复默认人格 python main.py --show-constitution # 看宪法 ``` 交互模式里同样能改:`/persona`、`/persona set 语气=含蓄`、`/persona reset`、 `/constitution`、`/reload`(立刻重读两个文件)。 ### persona.md 长什么样 小节名就是字段,`## 性格` 下面加一行 `- ……` 就是加一条性格。认不出的小节会自动跳过, 少写了的字段用默认值补齐 —— 手改坏了也不会让程序起不来。 ```markdown # 人格(Persona) - 名字:shadow - 代号:影 - 更新于:2026-09-07 16:04:20 ## 身份 你是 shadow(影),一个安静而锋利的 AI Agent…… ## 性格 - 冷静克制:不夸张、不煽情、不迎合 - 诚实:知道就说知道,不知道就直说不知道 ``` > 旧版是 `persona.json`。现在若发现它,会自动把内容迁移成 `persona.md`, > 旧文件改名保留为 `persona.json.bak`,确认无误后自行删除即可。 ### 存到哪了? - 默认在**项目根目录**:`persona.md`、`CONSTITUTION.md`(建议进 git,方便版本化与分享) - 对话记忆:`memory.json`,加 `--remember` 才会读写(已 gitignore,含隐私) - 想挪位置:设环境变量 `SHADOW_STATE_DIR=~/.shadow` - 写入一律用**原子写**(先写 `.tmp` 再 `os.replace`)—— 写一半崩掉不会留下半残配置,这种 bug 最难排查 --- ## 五、给它装一只新"手" 加工具只需要写一个函数,然后注册一行。完整可运行示例见 `examples/add_your_own_tool.py`: ```python from shadow import Agent, MockLLM, ToolRegistry def word_count(text: str) -> str: """统计一段文字的字数。""" # ← 这句 docstring 就是给模型看的说明 return str(len(text)) registry = ToolRegistry() registry.register(word_count, description="统计文本字数,用户问'这段话多长'时使用") agent = Agent(llm=MockLLM(), tools=registry) ``` 三个要点: 1. **描述比代码更重要** —— 模型是读着 `description` 决定用不用、怎么用的; 2. **参数 schema 自动推导** —— 从类型注解生成(`tools.py::_infer_parameters`), 想更精确就手动传 `parameters=...`; 3. **出错不要抛异常** —— 返回一段人话错误信息,模型会自己纠正重试。 --- ## 六、内置工具与安全边界 | 工具 | 作用 | 安全设计 | | --- | --- | --- | | `calculator` | 算数学表达式 | 用 `ast` 白名单求值,**禁用 `eval`**(`__import__('os')...` 会被拒绝);防除零、防超大指数 | | `get_current_time` | 当前日期时间 | 模型本身没有"现在"的概念,必须查工具 | | `read_file` | 读文本文件 | 限制在工作目录内,超长自动截断 | | `write_file` | 写文件 | 只能写 `outputs/` 沙箱目录,遵循最小权限原则 | 工具层统一原则:**任何异常都转成文字返回给模型,绝不让 Agent 崩掉。** --- ## 七、目录结构 ``` shadow/ agent.py ★ ReAct 主循环 + 模型输出解析(最该先读的文件) llm.py 大脑抽象基类 + 离线假大脑 MockLLM openai_llm.py 真实大模型客户端(urllib 手写,OpenAI 兼容协议) tools.py 工具注册与调度(参数校验、类型纠偏、异常兜底) builtins.py 4 个内置工具 memory.py 短期记忆(滑动窗口)+ 持久化到 memory.json persona.py 人格:性格/语气/原则/边界(Markdown 持久化,可热改) constitution.py 顶层设定(宪法):最高优先级、防提示词注入 state.py 持久化基础设施:路径解析 + 原子读写 prompts.py 系统提示词模板 config.py 配置(.env + 环境变量) main.py CLI 入口 tests/ 56 个单元测试 examples/ 扩展示例 ``` --- ## 八、已知局限与进阶方向 本项目刻意保持"最小可理解",因此**故意**没有做下面这些。 理解它们,你就知道工业级框架在解决什么问题: | 局限 | 进阶方向 | | --- | --- | | 靠正则解析文本协议,模型一"自由发挥"就出错 | 用 **Function Calling / tool_choice**,让 API 直接返回结构化调用 | | 只有最近 N 条消息的短期记忆 | 长期记忆:把历史存进向量库(FAISS / Chroma),按需检索 | | 单Agent 单线程 | 多 Agent 协作(规划者 / 执行者 / 审查者) | | 同步阻塞,要等全部生成完 | 流式输出(SSE),边生成边显示 | | 提示词全靠手写 | 自动优化提示词、加 Few-shot 样例 | | 工具出错靠模型自觉纠正 | 加"反思"环节、结构化重试与降级策略 | --- ## 九、开发里程碑 - [x] **M1** 骨架 + 最小可运行 ReAct 循环 - [x] **M2** 工具系统 + 内置工具 - [x] **M3** 接入真实大模型 - [x] **M4** 记忆 / 交互 / 自测 / 文档 - [x] **P1** 人格 + 顶层设定(宪法)+ 持久化 - [x] **P2** 人格热改 CLI / 记忆持久化 / 23 项新增自测 每个里程碑都是可运行状态,可用 `git log --oneline` 回看每一步的演进。