这个 Agent 接收用户粘贴的 URL 或文字,让 LLM 提取标题、摘要和标签,保存到数据库,并跳转到书签列表。
**本实战涵盖:** 提示词、MCP 工具、Create Hook、Next Hook、`ctx.memory.context`、`ctx.Send action: navigate`。
---
## 目录结构
```
assistants/bookmark/
├── package.yao
├── prompts.yml
├── prompts/
│ └── edit.yml
├── mcps/
│ └── store.mcp.yao
└── src/
└── index.ts
```
---
## 第一步:`package.yao`
```json
{
"name": "Bookmark Assistant",
"connector": "$ENV.DEFAULT_CONNECTOR",
"options": { "temperature": 0.3 },
"mcp": {
"servers": [
{
"server_id": "agents.bookmark.store",
"tools": ["save_bookmark", "get_bookmark"]
}
]
}
}
```
`server_id` 格式:`agents.<assistant路径点分隔>.<mcp文件名>`。Assistant 位于 `assistants/bookmark/`,ID 为 `bookmark`,所以 server_id 是 `agents.bookmark.store`。
低 temperature 确保 JSON 输出一致。
> **MCP 工具 vs Hook 中直接调用 Process:** MCP 工具暴露给 LLM 以便自动调用。在这个 Agent 中,LLM 只需返回 JSON 文本,无需调用工具 —— `Next` Hook 直接用 `Process()` 保存。注册 MCP 工具是为了让 LLM 在需要时能通过 `get_bookmark` 查询已有书签。
---
## 第二步:默认提示词(`prompts.yml`)
```yaml
- role: system
content: |
You are a bookmark assistant. Extract structured information from URLs or text.
When given input, respond with ONLY valid JSON in this exact format:
{
"title": "concise title",
"description": "2-3 sentence summary",
"tags": ["tag1", "tag2", "tag3"],
"source_url": "https://... or null"
}
Rules:
- title: specific, not generic (max 80 chars)
- description: what this is about, why it matters
- tags: 3-5 keywords, lowercase, relevant
- source_url: the URL if provided, null if text input
```
---
## 第三步:编辑提示词(`prompts/edit.yml`)
```yaml
- role: system
content: |
You are a bookmark assistant in edit mode.
The user wants to update an existing bookmark. Return ONLY valid JSON in
the same format as the create mode, incorporating the user's changes
while preserving information that wasn't changed.
Existing bookmark data will be injected as context.
```
---
## 第四步:MCP 工具(`mcps/store.mcp.yao`)
```json
{
"label": "Bookmark Store",
"description": "Save and retrieve bookmarks",
"transport": "process",
"tools": {
"save_bookmark": "models.bookmark.Save",
"get_bookmark": "models.bookmark.Find"
}
}
```
使用 `bookmark` 数据模型。模型定义见进阶章节。
---
## 第五步:Hook(`src/index.ts`)
```typescript
import { agent, log, Process } from "@yao/runtime";
// ── Create Hook ────────────────────────────────────────────────
export function Create(
ctx: agent.Context,
messages: agent.Message[]
): agent.Create {
// 为 Next Hook 存储计时和会话 ID
ctx.memory.context.Set("start_time", Date.now());
ctx.memory.context.Set("chat_id", ctx.chat_id);
// 保存用户输入
const lastMsg = messages[messages.length - 1];
if (lastMsg?.content) {
ctx.memory.context.Set("user_input", lastMsg.content);
}
// 检测编辑模式:URL 包含 ?id=xxx
const idMatch = ctx.route?.match(/[?&]id=([^&]+)/);
if (idMatch) {
const id = idMatch[1];
ctx.memory.context.Set("mode", "edit");
ctx.memory.context.Set("record_id", id);
// 注入已有记录,让 LLM 可以基于它更新
const existing = Process("models.bookmark.Find", Number(id), {});
if (existing) {
const enriched = [
{
role: "system" as const,
content: `已有书签数据:\n${JSON.stringify(existing, null, 2)}`,
},
...messages,
];
return { messages: enriched, prompt_preset: "edit" };
}
}
ctx.memory.context.Set("mode", "create");
return { messages };
}
// ── Next Hook ──────────────────────────────────────────────────
export function Next(
ctx: agent.Context,
payload: agent.Payload
): agent.Next | null {
if (payload.error) {
log.Error("Bookmark agent error: " + payload.error);
ctx.Send({ type: "error", props: { message: payload.error } });
return { data: { status: "error" } };
}
const raw = payload.completion?.content;
const content = typeof raw === "string" ? raw : "";
// 解析 LLM 响应中的 JSON
let bookmark: any;
try {
const match = content.match(/```json\n([\s\S]*?)\n```/);
bookmark = JSON.parse(match ? match[1] : content);
} catch (e) {
ctx.Send("无法解析响应,请重试。");
return { data: { status: "parse_error" } };
}
const mode = ctx.memory.context.Get("mode") as string || "create";
const chatId = ctx.memory.context.Get("chat_id") as string || ctx.chat_id;
let id: number;
if (mode === "edit") {
const recordId = ctx.memory.context.Get("record_id") as string;
id = Number(recordId);
Process("models.bookmark.Save", { id, ...bookmark }); // 更新已有记录
} else {
id = Process("models.bookmark.Save", { ...bookmark, chat_id: chatId }) as number;
}
// 导航到书签列表
ctx.Send({
type: "action",
props: {
name: "navigate",
payload: { route: `/agents/bookmark/list?highlight=${id}` },
},
});
// 带计时的成功消息
const duration = Date.now() - (ctx.memory.context.Get("start_time") as number);
const verb = mode === "edit" ? "已更新" : "已保存";
ctx.Send(`${verb}:**${bookmark.title}**(${duration}ms)`);
return { data: { status: "success", id, mode } };
}
```
---
## 测试
```bash
yao agent test -n bookmark -i "https://go.dev"
yao agent test -n bookmark -i "Clean Architecture is a software design philosophy..."
```
期望输出:LLM 返回 JSON,书签保存成功,出现导航动作。
---
## 总结
你构建了一个完整的端到端 Agent:
| 文件 | 作用 |
|------|------|
| `package.yao` | 配置 connector + MCP 工具 |
| `prompts.yml` | 定义 LLM 角色和输出格式 |
| `prompts/edit.yml` | 编辑模式覆盖提示词 |
| `mcps/store.mcp.yao` | 将数据库操作映射为 MCP 工具 |
| `src/index.ts` | Create:检测模式、注入上下文 —— Next:解析、保存、导航 |
提示词 + MCP + Create/Next Hook + 记忆 —— 这个模式是所有 Yao Agent 的基础。CLI Agent 使用完全相同的结构,唯一区别是把 LLM 换成了外部 CLI Agent。
→ **[第三章:CLI Agent](../cli-agent)**