如何构建 AI Agent:从 LLM 到工作环境

把一条报错贴给大语言模型,它通常能列出一长串可能原因。但如果我们换一种问法:
payment 服务一直在重启,帮我检查一下当前环境,找到证据,再给出处理建议。
模型要做的就不只是解释报错了。它需要知道去哪里查、能调用哪些工具、如何根据返回结果继续调查,以及什么时候证据已经足够。
这篇文章用一个假设的 Kubernetes 故障排查任务,把这些环节串起来。第一版的目标很小:读取服务状态和日志,给出有依据的判断。先把这一件事做好,再考虑更多能力。
文章适合了解 API、希望理解 Agent 架构的开发者。无需熟悉 Kubernetes 的全部细节:这里只需要知道,Pod 是运行容器的基本单位,Deployment 管理一组 Pod,日志和状态是排查问题的线索。示例用于解释设计,不是一套绑定特定 SDK 的可运行教程。
1. 先让 Agent 跑完一个循环
假设 Agent 收到了前面的请求,一次调查可能这样展开:
| 步骤 | 发生了什么 | 谁负责 |
|---|---|---|
| 1 | 把用户请求、调查范围和工具定义交给模型 | 运行时 |
| 2 | 请求查看 payment 命名空间下的 Pod | 模型 |
| 3 | 执行查询,返回 Pod 状态 | 运行时与工具 |
| 4 | 发现容器反复退出,请求读取上一次运行的日志 | 模型 |
| 5 | 返回日志,把新的证据加入上下文 | 运行时与工具 |
| 6 | 整理已知事实、解释可能原因,指出还缺什么证据 | 模型 |
这里最重要的机制是:工具结果会回到模型面前,影响下一步行动。 这就是 Agent Loop,也就是执行循环。
下面是一段接近 Python 的伪代码。它省略了具体 API 格式,但保留了消息之间的关系:
def run_agent(history, state, tools, max_steps):
for _ in range(max_steps):
context = build_context(history, state)
response = llm(context, tools)
history.append(response) # 保存模型响应,包括工具请求
if not response.tool_calls:
return response # 本轮交回回答,不代表任务必然成功
results = []
for call in response.tool_calls:
# 先校验参数和权限,再执行;普通工具错误也转成结果
result = execute_checked(call.name, call.arguments)
results.append(tool_result(call_id=call.id, data=result))
# 按所用 API 的格式记录,并保留请求与结果的对应关系
history.extend(format_tool_results(results))
update_state(state, response, results)
return stopped("达到执行步数上限")
不能只把工具结果追加进上下文,还要保存模型发出的工具请求,并让结果对应到调用 ID。例如 Claude 的工具接口要求结果通过 tool_use_id 关联原请求,并遵守消息顺序。官方工具调用文档
同样,“模型没有继续调用工具”只表示它暂时交回了回答。它可能完成了调查,也可能需要用户补充信息,或者因为权限不足无法继续。任务是否成功,需要按任务目标另外判断。 超时、预算和失败处理也需要在实际实现中补齐。
在本文中,我们把 LLM Agent 理解为:
由模型根据目标和反馈决定下一步行动、由运行时管理上下文并执行工具的软件系统。
模型周围负责这些事情的软件,称为 Agent Harness。Agent 包括模型和 Harness,两者的组合决定它实际能做什么。
2. LLM 在这个循环里做什么?
回到最底层,主流自回归语言模型生成文本的过程,可以简化为:
输入 → Token 序列 → 嵌入表示 → Transformer
↓
下一个 Token 的概率分布
↓
选择 Token,继续生成
Token 可以理解为模型处理文字的片段。Transformer 通过注意力机制等计算,为候选 Token 生成得分,再通过解码策略选出下一个 Token。温度、top-p 等参数影响选择方式;理解 Agent 架构时,暂时不需要深入这些细节。
对我们的故障排查任务来说,模型生成的内容可能是一段解释,也可能是一条结构化工具请求。它本身没有执行查询,是外围软件解释并执行了这个请求。
LLM 是一个概率性组件,输出可能出错,也可能在不同运行中选择不同的调查路径。Agent 工程要做的,就是把这样的组件接入有明确接口、权限和反馈的软件系统。
3. 上下文:每次调用,模型究竟看到了什么?
假设 get_pods 已经返回了异常状态,但下一次调用模型时,应用忘了把结果放进去。模型并不会因为“工具刚才执行过”就自动知道结果。
对应用中的即时信息而言,模型能使用的是本次调用里实际可见的内容。典型的上下文包括系统指令、用户请求、工具定义、相关对话、工具结果,以及检索到的资料。
这里要区分四个概念:
| 概念 | 含义 | 在排查任务中的例子 |
|---|---|---|
| History:历史 | 过去发生过什么 | 调用了哪些工具,各自返回了什么 |
| State:状态 | 任务目前进行到哪里 | 已检查 Pod,尚未检查上次退出的日志 |
| Memory:记忆 | 未来可能继续用到的信息 | 这个服务过去发生过哪类故障 |
| Context:上下文 | 本次实际交给模型的信息 | 当前目标、工具定义、最近的状态和日志片段 |
历史、状态和记忆都是上下文的潜在来源,但不等于上下文本身。 决定哪些信息进入本次调用,就是 Context Engineering,也就是上下文工程。
上下文窗口有 Token 容量上限,不能把所有历史无限追加。日志尤其容易占满窗口:几千行重复报错可能挤掉真正有用的退出原因。
因此,运行时需要保留重要状态、压缩旧对话、丢弃过期输出,并在必要时重新读取原始信息。例如,可以把调查进度压缩为:
已确认 payment-api 的容器多次重启,上次终止原因是 OOMKilled;尚未查看内存用量历史,不能判断是否存在内存泄漏。
好的摘要既保留事实,也保留尚未解决的问题。原始证据仍应能追溯,避免一次压缩把不确定的猜测变成后续调用中的“已知结论”。
4. 工具调用:把一次调查展开来看
LLM 可以生成 kubectl get pods 这串文字,但只有运行时实际执行查询后,它才拿到了当前环境的数据。
我们为第一版 Agent 提供两个自定义工具:
get_pods(namespace)
get_logs(namespace, pod, container, previous, tail_lines)
前者返回经过筛选的 Pod 和容器状态;后者读取指定容器的日志。previous=true 表示读取上一个已退出容器实例的日志,tail_lines 用于限制输出量。这两个参数分别对应 kubectl logs 的 --previous 和 --tail 选项。日志命令参考
以下 JSON 都是应用层的示意格式,不对应某个模型厂商的完整协议。模型首先请求:
{
"id": "call_01",
"tool": "get_pods",
"arguments": { "namespace": "payment" }
}
工具返回一份假设的状态快照:
{
"call_id": "call_01",
"pods": [{
"name": "payment-api-7d9c-example",
"container": "api",
"restart_count": 8,
"last_termination_reason": "OOMKilled",
"memory_limit": "256Mi"
}]
}
OOMKilled 表示容器因内存不足被终止。容器超出内存限制可能触发这种情况,但看到这个状态,还不能直接断言应用发生了内存泄漏。Kubernetes 内存资源说明
模型接着请求:
{
"id": "call_02",
"tool": "get_logs",
"arguments": {
"namespace": "payment",
"pod": "payment-api-7d9c-example",
"container": "api",
"previous": true,
"tail_lines": 100
}
}
假设返回的日志片段记录了进程退出前正在执行批量导入。一个合适的回答可以是:
当前记录显示,容器上次因 OOM 被终止,日志显示退出前正在执行批量导入。可以先沿内存使用方向调查,但目前不能确认是导入峰值、限制配置偏低,还是内存泄漏。下一步需要查看对应时段的内存曲线和任务规模。
这个回答把观察到的事实、尚未证实的解释和下一步检查分开了。它也没有假装已经查看自己拿不到的指标。
到这里,我们已经有了一个能参与实际工作的最小 Agent。后面的能力,都是为了处理它继续工作时遇到的具体问题。
5. RAG:当答案藏在外部资料里
状态和日志说明了发生什么,但可能没有说明团队应该如何处理。假设内部排障手册记录了批量导入的内存配置要求,模型需要先找到那段资料。
这就是 Retrieval-Augmented Generation(检索增强生成,RAG) 的用途:先检索相关信息,再将它交给模型生成回答。
资料 → 检索 → 相关片段 → 上下文 → 带依据的回答
RAG 不等于向量数据库。应根据问题选择检索方式:
| 信息需求 | 可用的检索方式 |
|---|---|
| 精确服务名、错误消息、配置项 | 关键词搜索、grep、BM25 |
| 与问题语义相近的文档 | 向量搜索 |
| 结构化业务数据 | SQL |
| 实体之间的关系 | 图查询 |
| 最新外部状态 | API |
其中,BM25 是一种常见的关键词相关性排序方法。对精确报错或代码标识符,关键词搜索往往就是一个合适的起点。通过 SQL 或 API 取得的信息,也可以参与检索增强的回答过程,但并非每次 API 调用都需要称为 RAG。
资料较多时,可以结合关键词与向量搜索,再用 Rerank(重排序) 从候选结果中挑选更相关的片段。检索负责找到候选,重排序负责决定哪些内容值得占用上下文空间。
在我们的案例里,比“搜到一篇讨论 OOM 的文章”更有价值的,是找到适用于当前服务版本的内部说明。文档的来源、适用范围和更新时间,也应该跟随片段进入上下文。
6. 记忆:上次的经验,这次还能不能用?
RAG 通常关心“有哪些相关资料”,记忆更关心“以前发生过什么,哪些信息值得保留”。两者可以共享检索技术,但记忆还需要处理更新、冲突和遗忘。
例如,Agent 保存过这样一条记录:
payment-api 曾因批量导入触发 OOM,当时容器内存限制为 256Mi。
后来团队已经修改了配置。下次再次发生重启,如果 Agent 只检索到旧记忆,就可能把过期配置当成当前事实。
因此,记忆不仅需要相关性,还需要时间、来源、适用范围和是否已被取代等信息。历史经验可以帮助提出调查方向,当前状态仍然需要重新确认。
从概念上,可以区分:
- 情景记忆:某次故障发生了什么,做过哪些检查。
- 语义记忆:从经历中整理出的知识,例如团队确认过的排障步骤。
一个简单的流程是“事件记录 → 提取事实 → 存储 → 按任务检索”。是否需要复杂的向量记忆系统,取决于任务;第一版也可以先保存结构化调查记录。
7. MCP:什么时候值得统一工具接口?
前面的两个工具可以直接通过 Kubernetes API 或 SDK 实现。当其他应用也想使用这些能力时,重复集成才开始成为问题。
Model Context Protocol(模型上下文协议,MCP) 提供了一种标准化的能力集成方式:
Agent 应用 → MCP Client → MCP Server → 外部系统
MCP Server 可以暴露工具及其参数定义,应用通过协议发现和调用。工具调用是基本能力,MCP 是工具及其他上下文能力的集成层。MCP 架构说明
对于我们的排查助手,可以先直接调用 API。等多个应用需要复用 get_pods、get_logs 等能力时,再考虑用 MCP 封装。它改变的是连接方式,工具是否允许访问某个命名空间,仍需要权限机制决定。
8. Harness 与工作流:谁决定下一步,谁负责执行?
前面的上下文构建、历史记录、工具执行和权限处理,都属于 Harness 的职责。它还需要处理压缩、重试、超时、执行循环、审批和追踪。
这也解释了为什么使用相近模型的两个 Agent,表现仍可能不同:一个把关键日志放进上下文,另一个截断了它;一个保留调查进度,另一个每轮都重新查起。模型能力相近,工作过程仍可能相差很大。
不过,并不是每一步都需要模型决定。Workflow(工作流) 由开发者定义执行路径;Agent 则在授权范围内根据反馈动态选择行动。
在排查任务里,两者可以这样分工:
| 环节 | 合适的控制方式 |
|---|---|
| 确认用户身份、限定可访问的环境 | 固定程序逻辑 |
| 根据状态选择继续查日志还是指标 | 模型动态决定 |
| 检查工具参数和权限 | 固定程序逻辑 |
| 整理证据,解释仍存在的不确定性 | 模型生成 |
| 对后续变更执行审批和操作 | 工作流与权限策略 |
如果某类告警总是执行完全相同的三步检查,普通工作流可能已经足够。Agent 更适合下一步依赖现场信息、事先难以穷举所有路径的环节。
9. 结构化输出和安全护栏:能解析,不等于能执行
假设以后要让助手提出变更建议,程序更容易处理这样的结构化结果:
{
"action": "restart",
"namespace": "payment",
"deployment": "payment-api"
}
Schema 定义字段、类型和允许值,让模型与软件之间有一个明确接口。但需要区分:
结构合法 ≠ 语义正确 ≠ 获得授权 ≠ 安全。
这段 JSON 即使完全符合 Schema,重启也未必适合当前的内存问题。应用还要确认目标是否存在、用户是否有权限,以及操作是否符合执行策略。
安全护栏应落在实际执行路径上:校验参数和语义、检查身份与权限、应用风险策略,必要时审批,最后执行并记录结果。模型输出应作为待校验的输入。
工具设计也要遵循最小权限原则。第一版助手只需要查状态和日志,就先提供这两种能力。限制可访问的命名空间、日志范围和输出量,比给它任意 Shell 执行权限后再写一句“谨慎操作”更容易控制。
这里的关键是:能力控制比提示词约束更有力。 如果要限制删除资源,既要避免直接提供删除工具,也要避免通过通用命令或 API 获得同样的权限。
一种执行策略可以是:
| 操作类型 | 执行策略示例 |
|---|---|
| 授权范围内的普通状态查询 | 自动执行 |
| 可能包含敏感信息的日志读取 | 限定范围并处理敏感字段 |
| 低风险写操作 | 按明确策略执行 |
| 生产环境变更 | 按团队要求审批 |
| 不属于任务范围的高风险能力 | 不提供 |
只读也需要权限,但不必把每一次普通查询都变成人工确认。控制边界应由任务和数据决定。
10. Evals:这次答对了,下次呢?
一次排查成功,只能说明这一次跑通了。要判断助手是否值得在工作中依赖,需要 Evals(评估)。
先给排查任务定义成功标准:找到与证据一致的原因,引用实际获得的信息,说明证据不足之处,并遵守只读范围。回答流畅只是其中很小的一部分。
评估案例需要包含可复现的状态、日志或工具返回,而不只是一个报错标签。可以从下面几类样例开始:
| 案例 | 提供的证据 | 预期表现 |
|---|---|---|
| 内存相关退出 | 容器终止状态为 OOMKilled,附日志 | 指出内存方向,不凭空断言内存泄漏 |
| 服务无可用后端 | Service 的筛选条件与 Pod 标签不匹配 | 识别 selector 不匹配,即服务选错了 Pod |
| 镜像拉取失败 | 事件中出现仓库认证错误 | 判断认证问题,不泛泛建议重启 |
| 日志读取被拒绝 | 工具返回权限错误 | 说明信息缺口,不编造日志内容 |
后两类调查需要扩展工具或使用对应的模拟返回;只有前文两个工具的第一版,应先评估它实际能完成的任务。
能确定性检查的部分,就直接检查:是否调用了禁止的工具、参数是否越界、引用的 Pod 是否存在于返回中。对于诊断解释的质量,再结合规则评分、人工抽查或 LLM 评判器。模型评判器本身也需要校准,不能成为新的盲点。
评估还应该用于回归。假设两个版本得到下面这组示例结果:
| 指标 | v1 | v2 |
|---|---|---|
| 任务成功率 | 82% | 88% |
| 安全指标通过率 | 99% | 96% |
| 单次成本 | $0.04 | $0.09 |
只看成功率,会想升级;加上安全性和成本,决定就没那么简单了。“安全指标通过率”也需要明确口径,例如多少次运行没有违反预设权限策略。
这些数字只是当前评估集上的观测结果。判断改善是否稳定,还要在相同环境下多次运行,结合样本量和结果波动,而不是把一次跑分当成定论。评估既要看执行记录,也要看任务的真实结果。Agent 评估实践
提示词、模型、检索和工具发生变化时,都可以重跑同一套评估,检查收益和退步发生在哪里。
11. 可观测性:证据有了,为什么还答错?
假设工具明明返回了 OOMKilled,Agent 却建议优先排查网络。问题可能出在两个完全不同的地方:
- 关键字段没有进入模型的实际上下文,或者被压缩掉了。
- 模型看到了这个字段,却没有正确使用它。
仅看最终回答,很难区分。Tracing(链路追踪) 要把一次运行中的上下文构建、模型调用、工具请求、工具结果和结束状态关联起来。
对第一版助手,先记录以下信息就有帮助:
| 记录内容 | 能帮助回答的问题 |
|---|---|
| 实际发送的上下文与检索片段 | 关键证据有没有交给模型? |
| 工具名称、参数、结果与错误 | 查的是不是正确环境?调用成功了吗? |
| 模型耗时、Token 用量、缓存命中量 | 时间和费用花在哪里? |
| 执行轮数、停止原因和最终结果 | 为什么结束?目标完成了吗? |
记录内容同样要遵守数据访问和保留规则,尤其是日志中的敏感信息。
评估帮助发现“这个版本在哪类任务上表现差”,追踪帮助定位“这一次具体在哪一步出了问题”。这与分布式系统排错很相似,只是部分执行路径由模型在运行时决定。
12. 让它稳定工作,再考虑节省重复计算
工具超时、模型服务失败、相同查询重复执行,这些都不是提示词能独自解决的问题。运行时需要明确的退出和恢复机制。
对排查助手,可以先处理三件事:
- 给运行设上限。 限制步数、耗时和预算;达到上限时,返回已有证据与未完成事项。
- 让重试有边界。 临时查询失败可以有限重试,并逐渐延长间隔;权限错误应交代原因。以后加入写操作时,要防止重试造成重复执行,这就是幂等性需要解决的问题。
- 保留可恢复的进度。 保存重要状态和工具结果,恢复时重新确认可能已变化的环境数据。
任务规模扩大后,再按需要加入限流、熔断、并发控制和检查点。熔断是在服务持续失败时暂时停止调用;检查点则保存可用于恢复的执行进度。
稳定运行后,可以观察 Prompt Caching(提示词缓存) 是否能降低重复计算。排查过程里的多次请求,往往共享系统指令和工具定义等前缀:
请求 A:稳定前缀 + 当前状态 A
请求 B:稳定前缀 + 当前状态 B
因果注意力使前面的 Token 不依赖后面的 Token,因此相同前缀对应的部分计算有机会复用。具体命中条件由模型服务决定,例如前缀匹配、缓存有效期和最小长度等要求。提示词缓存说明
它与语义缓存不同:提示词缓存复用相同前缀的计算;语义缓存查找相似问题的历史答案,并判断能否复用。对于不断变化的服务状态,即使用户问题完全一样,也不能直接把上次诊断当成这次答案。
13. 从一个小而完整的助手开始
现在可以把整个系统放回同一张图里:
用户目标
↓
Harness:管理历史、状态、权限与执行预算
↓
构建上下文 ← 工具证据 / 相关资料 / 有效记忆
↓
模型决定下一步
├── 请求工具 → 校验并执行 → 记录结果 → 下一轮
└── 交回回答 → 检查任务结果或等待补充信息
评估与追踪:观察整个过程及其结果
构建这样的助手,仍然是在做软件工程:定义接口、管理状态、控制权限、处理故障,并验证结果。模型增加了动态判断能力,Harness 负责让这种能力有稳定的工作条件。
如果现在动手,我会先把范围收在下面四步:
- 接入 Pod 状态和容器日志两个只读工具,限制到一个测试环境。
- 跑通“请求 → 查询 → 读取结果 → 继续调查 → 给出判断”的循环,保留请求与结果的对应关系。
- 用正常、异常、证据不足和权限拒绝四类样例,检查回答是否有依据、是否遵守范围。
- 根据失败记录决定下一步:缺资料就补检索,缺指标就加工具,遗失上下文就改状态管理。
等它能稳定完成这些任务,再加入长期记忆、更多集成或变更能力。一个能说明“我查到了什么、还不知道什么、下一步该查什么”的助手,已经可以在工作中提供明确的价值。