Hono实现Agent API
要点
- 前面十几篇分别讲了 Agent 的各个模块——工具定义、执行器、记忆系统、错误恢复、状态管理,这篇把它们串起来,用 Hono 搭一个完整的 Agent API 服务
- 整体架构分三层:Hono 路由接收请求、Agent Loop 驱动 LLM 与工具的交互循环、工具层执行具体操作
- 会话管理用 KV 存储对话历史,支持多轮对话和过期自动清理
- Agent 核心循环处理四种终止条件:LLM 给出最终回复、工具调用出错、循环次数上限、请求超时
- 流式响应通过 SSE 把 Agent 的思考过程和工具调用状态实时推送给前端
1. 整体架构
一个完整的 Agent API 需要处理这些环节:
前端请求 → Hono 路由 → 会话管理 → Agent Loop → LLM API
↓
工具调用 → 工具执行器 → 外部服务
↓
结果回传 → 流式响应 → 前端
按职责拆成三层:
- 接口层(Hono 路由)— 接收 HTTP 请求,做参数校验、鉴权、限流
- 编排层(Agent Loop)— 驱动「LLM 推理 → 工具调用 → 结果回传」的循环
- 工具层(工具注册表 + 执行器)— 管理工具定义和执行逻辑
目录结构:
src/
index.ts 入口文件
types.ts 共享类型
routes/
agent.ts Agent API 路由
agent/
loop.ts Agent 核心循环
llm.ts LLM 调用封装
session.ts 会话存储(KV)
stream.ts 流式响应
tools/
registry.ts 工具注册表
types.ts 工具类型
weather.ts 天气查询工具
middleware/
auth.ts 鉴权中间件2. 类型定义
所有共享类型集中到 types.ts:
// src/types.ts
export type Message = {
role: 'system' | 'user' | 'assistant' | 'tool'
content: string
toolCalls?: ToolCall[]
toolCallId?: string
}
export type ToolCall = {
id: string
name: string
arguments: Record<string, unknown>
}
export type LLMResponse = {
text: string
toolCalls: ToolCall[]
}
// Agent 事件(用于流式响应)
export type AgentEvent =
| { type: 'thinking'; content: string }
| { type: 'tool_call_start'; toolName: string }
| { type: 'tool_call_result'; toolName: string; result: unknown }
| { type: 'done'; finalText: string }
| { type: 'error'; message: string }
export type Session = {
id: string
userId: string
messages: Message[]
createdAt: number
updatedAt: number
}
export type AppEnv = {
Bindings: {
OPENAI_API_KEY: string
KV: KVNamespace
DB: D1Database
MAX_ITERATIONS: string
}
Variables: { user: { id: string; role: string } }
}3. 会话管理
前面的文章里,messages 数组只在内存中存在,请求结束就丢失。生产环境需要持久化对话历史。KV 读写延迟低、自带过期机制,适合存储会话数据:
// src/agent/session.ts
import type { Session, Message } from '../types'
const PREFIX = 'session:'
const TTL = 86400 // 24 小时
export async function createSession(kv: KVNamespace, userId: string): Promise<Session> {
const session: Session = {
id: crypto.randomUUID(),
userId,
messages: [],
createdAt: Date.now(),
updatedAt: Date.now(),
}
await kv.put(`${PREFIX}${session.id}`, JSON.stringify(session), { expirationTtl: TTL })
return session
}
export async function getSession(kv: KVNamespace, sessionId: string): Promise<Session | null> {
return kv.get(`${PREFIX}${sessionId}`, 'json') as Promise<Session | null>
}
// 追加消息时续期 TTL,活跃会话不会被清理
export async function appendMessages(kv: KVNamespace, sessionId: string, newMessages: Message[]) {
const session = await getSession(kv, sessionId)
if (!session) return null
session.messages.push(...newMessages)
session.updatedAt = Date.now()
await kv.put(`${PREFIX}${session.id}`, JSON.stringify(session), { expirationTtl: TTL })
return session
}会话量超过千级时,list 遍历所有 key 效率不高,建议改用 D1 存储会话索引,或按 userId 做 key 前缀缩小范围。
4. LLM 调用封装
把 LLM API 封装成独立模块,Agent Loop 只关心消息和工具,不关心具体调的是哪个提供商:
// src/agent/llm.ts
import type { Message, ToolCall, LLMResponse } from '../types'
import type { ToolDefinition } from './tools/types'
const TIMEOUT_MS = 30_000
export async function callLLM(
messages: Message[],
tools: ToolDefinition[],
apiKey: string,
): Promise<LLMResponse> {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'gpt-4o',
messages,
tools: tools.map((t) => ({
type: 'function' as const,
function: { name: t.name, description: t.description, parameters: t.parameters },
})),
tool_choice: 'auto',
}),
signal: AbortSignal.timeout(TIMEOUT_MS),
})
if (!res.ok) throw new Error(`LLM API 错误 (${res.status})`)
const data = await res.json() as {
choices: Array<{ message: { content: string | null; tool_calls?: Array<{ id: string; function: { name: string; arguments: string } }> } }>
}
const msg = data.choices[0].message
return {
text: msg.content ?? '',
toolCalls: (msg.tool_calls ?? []).map((tc) => ({
id: tc.id,
name: tc.function.name,
arguments: JSON.parse(tc.function.arguments || '{}'),
})),
}
}超时控制放在这一层。LLM 响应时间波动大,不设超时的话,一个卡住的请求会拖住整个循环。
5. Agent 核心循环
把前面所有模块串起来,加上生产环境需要的终止条件:
// src/agent/loop.ts
import type { Message, AgentEvent, AppEnv } from '../types'
import { callLLM } from './llm'
import { executeTool } from './tools/registry'
import type { ToolDefinition } from './tools/types'
export type StopReason = 'completed' | 'max_iterations' | 'error' | 'timeout'
export type AgentResult = {
finalText: string
messages: Message[]
stopReason: StopReason
iterations: number
}
const TOTAL_TIMEOUT_MS = 60_000
export async function runAgentLoop(
messages: Message[],
tools: ToolDefinition[],
env: AppEnv['Bindings'],
onEvent?: (event: AgentEvent) => void,
): Promise<AgentResult> {
const maxIterations = Number(env.MAX_ITERATIONS) || 10
const startTime = Date.now()
for (let i = 0; i < maxIterations; i++) {
// 检查总超时
if (Date.now() - startTime > TOTAL_TIMEOUT_MS) {
onEvent?.({ type: 'error', message: '执行超时' })
return { finalText: '处理时间过长,请稍后重试。', messages, stopReason: 'timeout', iterations: i }
}
let llmResponse
try {
llmResponse = await callLLM(messages, tools, env.OPENAI_API_KEY)
} catch (error) {
const msg = error instanceof Error ? error.message : 'LLM 调用失败'
onEvent?.({ type: 'error', message: msg })
return { finalText: msg, messages, stopReason: 'error', iterations: i }
}
// 没有工具调用 → LLM 已给出最终回复
if (llmResponse.toolCalls.length === 0) {
messages.push({ role: 'assistant', content: llmResponse.text })
onEvent?.({ type: 'done', finalText: llmResponse.text })
return { finalText: llmResponse.text, messages, stopReason: 'completed', iterations: i + 1 }
}
messages.push({ role: 'assistant', content: llmResponse.text, toolCalls: llmResponse.toolCalls })
onEvent?.({ type: 'thinking', content: llmResponse.text })
for (const toolCall of llmResponse.toolCalls) {
onEvent?.({ type: 'tool_call_start', toolName: toolCall.name })
const result = await executeTool(toolCall, env)
onEvent?.({ type: 'tool_call_result', toolName: toolCall.name, result })
messages.push({ role: 'tool', toolCallId: toolCall.id, content: JSON.stringify(result) })
}
}
// 达到最大轮次
onEvent?.({ type: 'error', message: '达到最大执行轮次' })
return { finalText: '任务未能完成。', messages, stopReason: 'max_iterations', iterations: maxIterations }
}四个终止条件:
completed— LLM 没有调用工具,已生成最终回复max_iterations— 循环达到上限,防止工具调用死循环error— LLM 调用失败timeout— 总执行时间超限,防止单个任务占用过多资源
onEvent 回调是流式响应的基础,每次循环中的关键事件都通过它推送出去。
6. 工具注册机制
前面 06 篇讲过工具执行器的设计。这里加上动态注册和权限过滤:
// src/agent/tools/types.ts
export type ToolDefinition = {
name: string
description: string
parameters: Record<string, unknown>
requiredRole?: string
execute: (args: Record<string, unknown>, env: Record<string, string>) => Promise<unknown>
}// src/agent/tools/registry.ts
import type { ToolCall, ToolDefinition } from './types'
const registry = new Map<string, ToolDefinition>()
export function registerTool(tool: ToolDefinition): void {
if (registry.has(tool.name)) throw new Error(`工具已注册:${tool.name}`)
registry.set(tool.name, tool)
}
// 按角色过滤:用户没权限的工具不会出现在发给 LLM 的列表里
export function getTools(userRole?: string): ToolDefinition[] {
const all = Array.from(registry.values())
if (!userRole) return all
return all.filter((t) => !t.requiredRole || userRole === 'admin' || userRole === t.requiredRole)
}
export async function executeTool(toolCall: ToolCall, env: Record<string, string>): Promise<unknown> {
const tool = registry.get(toolCall.name)
if (!tool) return { error: `未知工具:${toolCall.name}` }
try {
return await tool.execute(toolCall.arguments, env)
} catch (error) {
return { error: `工具执行失败:${error instanceof Error ? error.message : '未知错误'}` }
}
}新增工具只需要写一个模块并调用 registerTool,不需要改执行器或循环。例如:
// src/agent/tools/weather.ts
import { registerTool } from './registry'
registerTool({
name: 'get_weather',
description: '查询指定城市的当前天气信息',
parameters: {
type: 'object',
properties: { city: { type: 'string', description: '城市名称' } },
required: ['city'],
},
async execute(args) {
const res = await fetch(`https://api.weather.com/v1/${args.city}`, { signal: AbortSignal.timeout(5000) })
if (!res.ok) return { error: `天气查询失败:${res.status}` }
return res.json()
},
})7. 流式响应
Agent 调 3-4 个工具时可能耗时 15-20 秒,前端同步等待期间什么都看不到。用 SSE 把每一步状态实时推送出去:
// src/routes/agent.ts(流式接口部分)
import { Hono } from 'hono'
import { streamSSE } from 'hono/streaming'
import type { AppEnv, Message, AgentEvent } from '../types'
import { runAgentLoop } from '../agent/loop'
import { getTools } from '../agent/tools/registry'
import { getSession, createSession, appendMessages } from '../agent/session'
const agentApp = new Hono<AppEnv>()
// 同步接口
agentApp.post('/chat', async (c) => {
const { message, sessionId } = await c.req.json<{ message: string; sessionId?: string }>()
if (!message) return c.json({ error: 'message 不能为空' }, 400)
const user = c.get('user')
let session = sessionId ? await getSession(c.env.KV, sessionId) : null
if (!session) session = await createSession(c.env.KV, user.id)
await appendMessages(c.env.KV, session.id, [{ role: 'user', content: message }])
const messages = [...session.messages]
const result = await runAgentLoop(messages, getTools(user.role), c.env)
// 保存 Agent 产生的新消息
const newMessages = messages.slice(session.messages.length)
await appendMessages(c.env.KV, session.id, newMessages)
return c.json({ reply: result.finalText, sessionId: session.id, stopReason: result.stopReason })
})
// 流式接口:实时推送 Agent 执行过程
agentApp.post('/chat/stream', async (c) => {
const { message, sessionId } = await c.req.json<{ message: string; sessionId?: string }>()
if (!message) return c.json({ error: 'message 不能为空' }, 400)
const user = c.get('user')
let session = sessionId ? await getSession(c.env.KV, sessionId) : null
if (!session) session = await createSession(c.env.KV, user.id)
await appendMessages(c.env.KV, session.id, [{ role: 'user', content: message }])
const messages = [...session.messages]
return streamSSE(c, async (stream) => {
await stream.writeSSE({ event: 'session', data: JSON.stringify({ sessionId: session!.id }) })
const onEvent = async (event: AgentEvent) => {
await stream.writeSSE({ event: event.type, data: JSON.stringify(event) })
}
const result = await runAgentLoop(messages, getTools(user.role), c.env, onEvent)
const newMessages = messages.slice(session!.messages.length)
await appendMessages(c.env.KV, session!.id, newMessages)
})
})
export default agentApp前端收到的 SSE 事件流大致是这样的:
event: session
data: {"sessionId":"abc-123"}
event: thinking
data: {"type":"thinking","content":"需要先查询天气信息"}
event: tool_call_start
data: {"type":"tool_call_start","toolName":"get_weather"}
event: tool_call_result
data: {"type":"tool_call_result","toolName":"get_weather","result":{"temperature":28}}
event: done
data: {"type":"done","finalText":"北京今天28度,晴天"}前端根据 event 类型更新 UI:thinking 时展示思考动画,tool_call_start 时展示工具调用状态,done 时展示最终回复。
8. 入口文件与配置
// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import type { AppEnv } from './types'
import { authMiddleware } from './middleware/auth'
import agentApp from './routes/agent'
// 导入工具模块,触发 registerTool
import './agent/tools/weather'
const app = new Hono<AppEnv>()
app.use('*', logger())
app.use('*', cors())
app.use('/api/*', authMiddleware)
app.route('/api/agent', agentApp)
app.get('/health', (c) => c.json({ status: 'ok' }))
export default app// wrangler.jsonc
{
"name": "agent-api",
"main": "src/index.ts",
"compatibility_date": "2024-12-01",
"kv_namespaces": [{ "binding": "KV", "id": "xxxx" }],
"d1_databases": [{ "binding": "DB", "database_name": "agent-db", "database_id": "xxxx" }],
"vars": { "MAX_ITERATIONS": "10" }
}OPENAI_API_KEY 通过 wrangler secret put OPENAI_API_KEY 设置,不写在配置文件里。
9. 错误处理汇总
一个请求从进入到返回,各层可能遇到的错误和处理策略:
接口层 — 请求体缺字段返回 400,鉴权失败返回 401,会话不存在返回 404,无权访问返回 403。这些都是同步返回,不需要走 Agent Loop。
LLM 调用层 — API 限流(429)直接返回错误,不重试避免雪崩;网络超时 30 秒后终止循环;模型返回格式异常时捕获 JSON 解析错误。
工具执行层 — 参数校验失败和执行异常都返回给 LLM,让模型自行修正或放弃;单个工具 5 秒超时,返回超时错误。
循环层 — 达到最大迭代次数或总超时(60 秒)时返回提示文本,stopReason 标记终止原因。
核心原则:工具执行失败时把错误信息返回给 LLM,让模型自行决定重试、换参数还是放弃。LLM 在大多数情况下能根据错误信息做出合理判断。
总结
回顾一下这篇的要点:
- 完整的 Agent API 分三层:接口层处理 HTTP 和鉴权,编排层驱动 LLM 与工具交互,工具层管理定义和执行
- 会话管理用 KV 存储,支持多轮对话、TTL 自动过期、活跃续期
- Agent 核心循环有四个终止条件:LLM 给出最终回复、达到最大迭代次数、LLM 调用失败、总执行超时
- 工具注册采用 Map + 动态注册,新增工具只需写一个模块调用
registerTool,不改执行器或循环 - 流式响应通过 SSE 推送每一步状态,前端实时展示思考过程和工具调用进度
- 错误处理按层分类,工具层失败把错误返回给 LLM 让它自我修正
这篇把前面十几篇的模块组装成了一个可运行的 Agent API 服务。可以在此基础上继续扩展——接入更多 LLM 提供商、增加工具并发执行、接入向量数据库做长期记忆检索。