第 1 周 · Tether 源码课
从一次运行认识 Agent Harness
学完你能做到
- 区分 LLM、chatbot 与 agent harness
- 无 API key 跑通 Tether CLI 的一次完整 turn
- 从输出中找到 session、event、status 与 teardown 证据
- 用一条主链描述输入如何变成输出
课程进度
- 第 1 周
- 第 2 周
- 第 3 周
- 第 4 周
- 第 5 周
- 第 6 周
- 第 7 周
- 第 8 周
- 第 9 周
- 第 10 周
- 第 11 周
- 第 12 周
- 第 13 周
- 第 14 周
- 第 15 周
- 第 16 周
一句话先懂
LLM 负责“想出下一段内容”,agent harness 负责“让一次工作可靠地发生”。
Tether 把模型接到会话、工具、权限、持久化和生命周期上;没有这层 harness,模型只是在输入文本后继续生成文本。
今天不拆内部零件。目标只有两个:亲手跑一次,然后能指着输出说出“系统刚才做过什么”。
先看大图
一个类比:餐厅不是厨师
把 LLM 想成厨师:它根据订单做出一道菜。agent harness 更像整家餐厅:
| 餐厅里的角色 | Tether 中的角色 |
|---|---|
| 顾客写下需求 | 用户消息 |
| 前台接单 | CLI |
| 店长安排这一单的步骤 | Agent loop |
| 厨师产出菜品 | LLM 生成内容 |
| 工具间执行切配、清洗 | Tools |
| 订单流水记录发生过什么 | Session events |
| 门禁与操作规范 | Approval、Sandbox、Invariants |
类比边界:餐厅类比只帮助理解角色分工。它不能推出 Tether 的并发、异常、取消或持久化语义;这些都必须以源码和测试为准。
术语落地
| 术语 | 现在先这样理解 | 本周入口 |
|---|---|---|
| LLM | 接收上下文并流式生成内容的能力 | ReplayLlm 用预录内容模拟它 |
| Agent | 把一次用户请求组织成一个或多个 step | IAgent.SendAsync |
| Harness | 把模型与软件能力可靠连接起来的运行系统 | Tether 整体 |
| Turn | Agent 处理一批输入直到本轮停止 | turn/start 到 turn/end |
| Step | 一次模型请求及其可能的工具处理 | step/start 到 step/end |
| Session | 以 append-only event 记录已经发生的事实 | 输出中的 events 与 transcript |
先不要背类名。能在图和输出里找到它们,比会默写定义重要。
源码放大镜
入口是 apps/Tether.Cli/Program.cs。先看目的:它只负责读取进程参数、启动已经组合好的插件树、运行 CLI host,最后可靠回收。
// 生产组合挂真实 llm-multi;只有显式 --replies 才把发行 replay patch 追加为最后一层,
// 把 llm/default-model 换成脚本回放客户端。
ReplayLlm? llm = null;
if (replies is not null)
{
llm = new ReplayLlm();
llm.Load(replies);
patches.Add(BundleCatalogs.ReplayPatchPath);
}
// …
boot = await AppBoot.StartAsync(cliCatalog, bootOptions, cts.Token);
result = await boot.Context.Require<ICliHost>().RunAsync(cts.Token);
这几行完成三件事:
ReplayLlm从 JSON 文件读取预录回复,所以第一次运行不需要 API key; 发行组合本身始终挂真实 provider 行,--replies只会把bundles/replay.patch.yaml追加为最后一个 patch 层,把llm-multi/agent-default-model换成脚本回放。AppBoot.StartAsync根据 composition 挂载插件树。- CLI 从 Context 里取得
ICliHost,再把你的输入交给 Agent。
注意这里没有 new Agent(...)、new SqliteStore(...) 的长清单。具体实现由组合配置选择;第 7 周会追踪这条装配链。
动手实验:第一次完整 turn
实验目标:不用真实模型跑完一次 turn,并从结果中找到“开始、生成、结束、回收”的证据。
1. 预测
运行前,在纸上或临时文本里写下:
- 预录文件只有
hello-world,最终文本会是什么? - 一个没有工具调用的 turn 至少会留下哪些 start/end 事件?
- 命令结束时 Agent 应该是
Running还是Idle?
2. 运行
macOS / Linux:
course_home=$(mktemp -d)
dotnet run --project apps/Tether.Cli -- \
--home "$course_home" \
--replies apps/Tether.Cli/replies/text.json \
--send "Summarize this repository"
PowerShell:
$courseHome = Join-Path ([IO.Path]::GetTempPath()) ("tether-course-" + [guid]::NewGuid())
dotnet run --project apps/Tether.Cli -- `
--home $courseHome `
--replies apps/Tether.Cli/replies/text.json `
--send "Summarize this repository"
第一次会先编译项目,耐心等到输出两行 JSON。第一行是面向终端的文本结果;第二行是便于测试和诊断的完整 snapshot。
3. 观察
输出很长,先只找这些片段:
{"op":"text","text":"hello-world"}
{
"output": "ok",
"error": null,
"status": "Idle",
"events": [
{ "type": "turn/start" },
{ "type": "step/start" },
{ "type": "user/message" },
{ "type": "assistant/chunk" },
{ "type": "assistant/message" },
{ "type": "step/end" },
{ "type": "turn/end" }
],
"teardown": { "complete": true }
}
实际 events 还会按顺序出现 permission/preset、agent/inbox/spliced、system/message、
request/header、request/start 等事件(不同 seq 填充其间),本节略去具体 seq。
不要把上面的节选误认为完整日志。
4. 解释
hello-world来自apps/Tether.Cli/replies/text.json,不是模型理解了你的英文问题。status: Idle说明 Agent 已处理完 inbox,不再运行。turn/start、step/start与对应的 end 形成“括号”,让失败、取消和恢复有可检查边界。teardown.complete: true说明进程退出前插件树完成回收;能给出答案不等于能安全退出。
检查理解
1. 如果把 --send 文本换成别的内容,为什么回复仍然可能是 hello-world?
查看答案
因为本实验使用 ReplayLlm。它按预录文件返回内容,不根据提示词推理。这样测试就不依赖网络、密钥、模型更新或随机性。
2. assistant/message 已经出现,为什么还需要 step/end 和 turn/end?
查看答案
消息内容只说明模型产出过什么;end 事件说明对应工作边界已经正常闭合。工具调用、失败或取消时,内容事件与生命周期事件承担不同职责。
3. Tether 与 LLM 是同一个东西吗?
查看答案
不是。LLM 是 Tether 可替换的一项能力;Tether 是组织 LLM、工具、会话、权限与生命周期的 harness。今天甚至用 ReplayLlm 替代了真实模型。
本周带走
- 先把 Tether 看成“让一次 agent 工作可靠发生的系统”,而不是聊天界面。
- 一次成功运行至少有输入、turn/step 生命周期、模型内容、session 事实和 teardown 证据。
- ReplayLlm 让第一次运行完全可重复;后续每周都能沿同一份输出向内追踪。
下一周我们不再看运行结果,而是把六十多个项目压缩成一张可阅读的仓库地图。完整运行入口可随时回看快速开始。