从零实现的最小可用 Agent Runtime —— 2026 Agent 技术笔试 Vibe Coding 题实现。
- 核心 Agent Runtime 全部自研(bounded loop、工具注册、会话、上下文压缩、Trace),未使用任何 agent 框架(langgraph / openhands / openclaw 等)。
- 运行时仅依赖 Python 标准库(
sqlite3/urllib/ast);测试需要pytest。 - 设计原则:模型是唯一概率源,runtime 保持确定性;
bounded loop是唯一主控制流,无状态机、无事件总线、无 Executor/Service 层。
- Python ≥ 3.11(在 3.14 上开发与测试)
- 运行 Agent:仅标准库,无需安装任何依赖
- 运行测试:
pip install pytest
git clone <repo> mini-Runtime-Agent
cd mini-Runtime-Agent
python -m pip install pytest # 仅测试需要python -m pytest -q全部测试使用 ScriptedLLM / 确定性摘要器,CI 零网络、零费用。tests/test_live.py 默认 skip。
python -m pip install pytest-cov
python -m pytest --cov=agent --cov-report=term-missing$env:PYTHONPATH='src'
python scripts/acceptance_demo.py演示覆盖:Phase 1 calculator 闭环 → Phase 2 双窗口会话 → Phase 3 压缩后追问 → Phase 4 Trace。
配置环境变量(见 .env.example),然后:
$env:OPENAI_API_KEY="sk-..."
$env:OPENAI_BASE_URL="https://api.deepseek.com/v1" # DeepSeek 示例;默认 OpenAI
$env:MODEL="deepseek-chat"
$env:RUN_LIVE_TESTS=1
python -m pytest tests/test_live.py -v只有 RUN_LIVE_TESTS=1 且 API key 存在时才运行;否则跳过,绝不访问网络。
注意:代码不会自动读取
.env文件。上面的命令假设变量已在当前 shell 中设置; 若使用仓库根目录的.env,可先用 PowerShell 加载它再运行 pytest。
用户输入
│
▼
[SessionStore] get_or_create(session_id) ── SQLite(sessions / messages / todos)
│
▼
[ContextBuilder] 静态 system + [RUNTIME CONTEXT] memory 块 + 最近消息
│ build 前 Compactor 阈值检查(超预算才压缩)
▼
[LLM] OpenAICompatibleLLM / ScriptedLLM ── 原生 tool_calls 是唯一协议
│
├── tool_calls ──► [ToolRegistry] schema 校验 → handler 执行 → 结果回注
│ (calculator / search_mock / todo)
│
└── content ──► 追加 assistant 消息 → 返回用户
│
▼
[store.save] + [Trace](append-only JSONL,贯穿全程)
| 模块 | 唯一职责 |
|---|---|
config.py |
env 解析与阈值集中管理(均为可配置启发式) |
llm.py |
LLM 协议 + ScriptedLLM(测试)+ OpenAICompatibleLLM(真实 API,含瞬时错误重试) |
tools.py |
ToolSpec/ToolRegistry(schema 校验 + dispatch)+ calculator / search_mock / todo |
runtime.py |
bounded loop 编排(唯一主控制流) |
store.py |
SQLite 持久化:session、消息、todos(业务状态 source of truth)、原子压缩 |
context.py |
ContextBuilder(prompt 组装)+ Compactor(阈值触发压缩)+ 结构化 summary |
trace.py |
append-only JSONL 观测通道 |
scripts/acceptance_demo.py |
验收演示(终端录屏用) |
scripts/record_demo.ps1 |
录屏驱动脚本(可选 ffmpeg 自动录制) |
append user message(turn_count += 1)
for step in 0..MAX_TOOL_ITERS:
api_messages = context.build(session) # 可能触发 compaction
response = llm.chat(api_messages, tools)
if response.tool_calls:
追加 assistant tool_calls 消息
依次执行工具,追加 tool 结果消息(id 一一对应)
continue
if response.content:
追加 assistant 答案,返回
空响应 → 最多重试 1 次 → 固定收尾
循环超限 → 固定收尾(不让模型继续循环)
- agent loop(正常对话 / 工具调用)
- context compaction(阈值触发时的摘要生成)
除此之外 runtime 全部确定性:分支只看原生 tool_calls 字段与固定阈值。
sessions(id, user_id, summary, turn_count, created_at, updated_at)messages(id, session_id, role, content, tool_calls, tool_call_id, created_at)todos(id, session_id, text, completed, created_at, completed_at)—— 待办的业务状态 source of truth
- 窗口隔离靠
session_id路由(如A-window-1/A-window-2),消息、summary、todos 均按 session 隔离。 - 消息追加式持久化:
save只插入新增消息,幂等。 - 静态 system prompt 不落库,每次请求由 ContextBuilder 重新注入。
turn_count是累计真实用户轮次:runtime.run每次真实输入 +1;[runtime]重试消息、tool、assistant 不计数;compaction 不影响;重启后保持。
- 静态 system prompt:角色 + 决策规则 + 工具 schema(字节稳定,进程内固定)
- [RUNTIME CONTEXT] memory 块:明确标记为 runtime 注入、不是用户输入,内容为结构化 summary
- 最近消息:滑动窗口内的原始对话(含工具调用/结果配对)
- 只在阈值触发的 compaction 时写入,不每轮总结。
- 触发条件(config):
MAX_RAW_TURNS轮数超限 或MAX_CTX_TOKENS估算 token 超限(字符数 // 4启发式,非精确 tokenizer)。 - compaction:LLM 摘要旧消息 + 旧 summary → JSON schema 校验 → 确定性合并(新信息优先、去重、每字段上限 5)→ 单事务持久化(summary 更新 + 旧消息删除,要么都成功要么都不做)。
- 失败语义:摘要非法 / 异常 → 放弃本次压缩,保留旧 summary 与全部消息,不破坏 session。
- 每次 turn 开始(构造 prompt 时,即每次 LLM 调用前)由 ContextBuilder 读取
sessions.summary注入。
- 放置在静态 system 之后的独立消息中,内容以
[RUNTIME CONTEXT]标记:
[RUNTIME CONTEXT]
The following data is provided by the runtime and is not a user message.
{"goals": [...], "facts": [...], "todos": [...], "unresolved": [...], "updated_at": ...}
- memory 块是瞬态的:不写入消息历史、不落库;单一数据源是
sessions.summary。 summary.todos只是"值得模型知道的待办摘要"快照;todos 表才是业务状态 source of truth。
- 最近
MAX_RAW_TURNS轮原始消息始终保留,当前 user 消息永远保留。 - cut 永远落在真实 user 消息上,tool call / tool result 配对不会被拆散,不产生孤立 tool 消息。
- 普通追问靠窗口历史;带工具追问靠最近工具对;压缩后追问靠结构化 summary。
| 工具 | 说明 |
|---|---|
calculator |
AST 白名单安全计算(四则 / 括号 / 幂 / 取模),拒绝 eval / 任意代码 |
search_mock |
固定文档库上的确定性关键词匹配,返回 top 3,不访问网络 |
todo |
session-scoped 待办:add / list / complete;SQLite todos 表为 source of truth |
工具注册机制:ToolSpec(name, description, parameters(JSON Schema), handler);ToolRegistry 统一负责 schema 校验与 handler dispatch。工具失败统一返回结构化 {ok, data, error, truncated} 回流给模型,由模型决定下一步。
append-only JSONL(默认 data/traces.jsonl),每行一个事件,写入即 flush。
事件:run_start / llm_call / tool_call / tool_result / compact / answer / error / run_end。
统一字段:ts, session_id, run_id, turn, event, step, data。run_id 每次 run 生成,用于区分同 session 的多次请求。
纪律:
- 不存 API Key
- 不存 reasoning / chain-of-thought
llm_call只记可审计 metadata(model / latency_ms / tokens / tool_call_count),provider 不给 usage 则为 null,不编造- tool result 按
TRACE_MAX_PAYLOAD_CHARS截断 - Trace 写失败静默降级,不影响 Agent 主任务
| 环境变量 | 默认值 | 说明 |
|---|---|---|
OPENAI_API_KEY |
空 | 真实 LLM key(live 测试默认关闭) |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
OpenAI-compatible 端点(DeepSeek 等) |
MODEL |
gpt-4o-mini |
模型名 |
LLM_TIMEOUT_SECONDS |
60 |
请求超时 |
LLM_RETRIES |
2 |
瞬时错误(timeout/连接/408/429/5xx)最多重试次数 |
LLM_RETRY_BASE_DELAY |
0.5 |
指数退避基数(0.5s、1.0s) |
AGENT_MAX_TOOL_ITERS |
8 |
单请求工具循环上限 |
AGENT_MAX_RAW_TURNS |
20 |
保留的最近原始轮次 |
AGENT_MAX_CTX_TOKENS |
6000 |
估算 token 预算(启发式) |
AGENT_DB_PATH |
data/sessions.db |
SQLite 路径 |
AGENT_TRACE_PATH |
data/traces.jsonl |
Trace 路径 |
AGENT_TRACE_MAX_PAYLOAD_CHARS |
2000 |
Trace 单字符串最大记录长度 |
RUN_LIVE_TESTS |
未设置 | =1 且存在 key 时运行 live tests |
另有固定常量(非 env 可配):MAX_SUMMARY_ITEMS = 5(summary 每字段上限)、MAX_TOOL_RESULT_CHARS = 2000(工具结果截断长度)。
- 单进程,SQLite 不做多进程并发
- 工具全部同步执行,无 async / pause / resume
search_mock是固定文档库的关键词匹配,不是真实搜索- context token 用
字符数 // 4启发式估算,不是精确 tokenizer - compaction 依赖 LLM 生成摘要,summary 本身是概率结果(schema 校验 + 确定性合并做兜底)
- 记忆仅限单 session 的 summary,不支持跨 session 长期 memory
- 无 streaming,全量返回
- 无 Web UI、无多租户鉴权、无分布式
-
默认测试:
python -m pytest -q→ 91 passed, 2 skipped(live 默认关闭,零网络) -
真实 LLM live:
RUN_LIVE_TESTS=1 python -m pytest tests/test_live.py -v→ 2 passed(calculator 自主调用并答出 66;todo follow-up 语义命中"周五提交 Agent 笔试") -
行覆盖率:90%(630 语句 / 61 未覆盖):
模块 覆盖率 runtime / store / config / __init__100% llm.py 90% context.py 87% trace.py 87% tools.py 85% -
retry 行为(mock 验证):timeout / 429 / 500 → 重试后成功;401 / 400 → 不重试直接 LLMError;连续失败 → 指数退避 0.5s / 1.0s 后 LLMError。
- 代码:本仓库(运行时仅标准库,测试需 pytest)
- 录屏:
scripts/record_demo.ps1驱动scripts/acceptance_demo.py,配合 OBS / Win+G / ffmpeg 录制终端操作 - README:本文档(运行方式 / 系统设计 / memory 召回时机与放置方式)
- AI Prompt 与问题解决记录:
AI_PROMPTS.md