Agent开发从零到跑通:Agent Loop、工具调用与Harness评测
Agent 开发岗这两年从“听起来很玄”变成了招聘 JD 里实打实的一栏:工具调用、任务编排、记忆管理、评测回归、可观测性,一条都躲不开。很多人卡住的地方不是不会调 API,而是不知道该把一个 Agent 拆成哪几层、每层用什么方案验证、面试官追问“你的 Agent 为什么这么设计”时说不出取舍理由。这篇就把这条链路从零到跑通讲清楚,重点放在能落地的代码结构和验证方法上,而不是概念堆叠。
本文会围绕一条主线展开:先手写一个最小 Agent Loop,把“模型—工具—观察结果—再决策”的闭环跑通;再往上加工具调用规范、记忆压缩、规划分层;然后用 Harness 把 Agent 当成软件来跑回归测试;最后做服务化封装、批量任务和成本观察。这里面 DeepAgent 可以理解为面向长链路深度任务的编排方式,Harness 则是把模型、工具、权限、日志、判分包在一起的运行外壳,两者一个管“跑得远”,一个管“测得准”。
硬件门槛其实不高。真正跑通一个 Agent 循环,一块能带动 7B 到 14B 量级模型的消费级显卡就够做本地联调,甚至只做工具编排和流程验证时,直接调远程推理接口、本地只跑业务代码也完全可行。真正吃资源的是评测环节和长上下文场景:批量跑几百条任务、每条轨迹几十轮,Token 消耗和并发压力会比单次对话高一个数量级,这部分才是需要提前规划的。
下面的内容适合三类读者:准备转 Agent 方向、想知道面试到底考什么的开发者;已经在写业务代码、想把 LLM 接进现有系统但不清楚工程边界的后端同学;以及搭过 Demo、但一上量就崩、想知道怎么补评测和可观测性的同学。全程给可复制的代码模板,路径、模型名、端口按你自己的环境替换即可。
1. Agent 开发岗核心能力速览
先给一张能力表,把 Agent 岗真正考察的东西摆出来。这张表不是让你背,而是让你对照自己的短板决定学习顺序:如果你连 Function Calling 的参数校验都没写过,就别急着上多智能体;如果你没跑过任何评测集,面试聊“效果好不好”基本会被问穿。
| 能力项 | 具体内容 | 掌握标准 | 优先级 |
|---|---|---|---|
| Agent 循环 | 决策、工具调用、结果回填、终止条件 | 能不看文档手写 while 循环 | 最高 |
| 工具调用 | JSON Schema 定义、参数校验、错误回传 | 能处理工具抛错和非法参数 | 最高 |
| 上下文管理 | 裁剪、摘要压缩、Token 预算 | 长对话不崩、不丢关键信息 | 高 |
| 规划能力 | ReAct、Plan-and-Execute、任务分解 | 能解释两种范式的取舍 | 高 |
| 记忆系统 | 短期会话、长期向量库、结构化状态 | 能设计读写时机与失效策略 | 中高 |
| 多智能体 | 编排者-执行者、Handoff、并行分发 | 能说清什么时候不该用 | 中 |
| Harness 评测 | 任务集、判分器、轨迹记录、回归 | 能跑出可对比的通过率 | 高 |
| 服务化 | 流式输出、超时、重试、幂等 | 能扛住并发和断连 | 高 |
| 安全边界 | 提示注入防护、权限最小化、沙箱 | 高危工具有人工确认 | 高 |
| 成本与性能 | Token 统计、P95 延迟、调用次数 | 能给出优化前后对比 | 中高 |
表格里有几个点值得单独强调。第一,Agent 循环是整个岗位的地基,市面上所有框架,不管是哪家的编排库,底层都是同一个模式:把对话历史发给模型,模型返回要么是最终答案、要么是工具调用请求,执行工具后把结果作为新消息塞回历史,再发一次。理解这一点之后,框架就只是语法糖,你随时可以自己写一个。第二,评测能力是被严重低估的一项,很多候选人能做出 Demo,但说不出“我的改版让通过率从 62% 提到 78%”,这在面试里是很致命的空缺。第三,安全边界不是加分项而是底线,只要你的 Agent 能执行 shell、能读写文件、能发起网络请求,就必须回答清楚权限怎么控。
再补一句关于“深度任务”的理解。长链路任务和普通问答的差别在于错误会累积:单步 95% 正确率,二十步之后就只剩 36%。所以 DeepAgent 这类深度编排思路的核心不是让模型更聪明,而是通过子任务拆分、中间结果校验、失败重规划,把长链路的误差控制住。你如果能把这句逻辑讲明白,比会背十个框架名有用得多。
2. 适用人群、能力分级与使用边界
Agent 开发不是一个独立工种,它是后端能力加模型理解的交叉。从招聘要求看,绝大多数岗位默认你已经会写服务、懂接口、能看日志,然后再加一层 LLM 相关的知识。所以学习路径不应该从“提示词技巧”开始,而应该从“怎么把一个不确定输出的组件塞进确定的工程流程”开始。这也是为什么很多人学了一堆提示词模板,真上手做项目还是无从下手。
按能力可以粗分三级。入门级是能调通 API、写一个带两三个工具的 Agent、处理基本异常,这个阶段一到两周能到,重点是手熟。进阶级是能设计状态机、做上下文压缩、接 RAG、跑一套评测集,并且能解释每个设计选择的代价,这个阶段通常需要完整做完一个项目。高级是能做多智能体编排、做权限隔离和沙箱、把成本和延迟压到业务可接受的范围,并且能主导技术选型,这部分靠项目量堆出来,光看资料到不了。
需要明确的边界也不少。第一,Agent 不是万能的,任务如果步骤固定、输入格式稳定,用传统代码加规则就够了,硬套 Agent 只会让系统更难调试、更贵、更慢。第二,涉及自动化操作的能力必须限定范围:文件写入限制在指定目录、命令执行放在容器里、网络请求走白名单,任何“能删能改”的工具都要有人工确认或 dry-run 模式。第三,涉及个人数据、账号凭证、商业机密的场景,要么本地部署,要么明确数据处理协议,日志里不要落明文密钥。
还有一层合规提醒必须写清楚:如果你的 Agent 要处理人脸、声音、证件、合同、医疗记录这类敏感素材,必须确认素材来源合法、当事人授权明确,输出内容也要有审核环节。做内容生成类 Agent 时,批量产出不等于可以批量发布,人工复核这一步不能省。技术能力越强,越要在设计阶段就把这些约束写进流程,而不是等出问题再补。
3. 环境准备与工程脚手架
环境这块不用搞得太重。做 Agent 开发,绝大部分时间花在写业务逻辑和调试 Prompt 上,真正吃显卡的只有本地推理那一段。所以最优解是双轨:本地跑业务代码和评测脚本,模型走远程推理服务或者本地小模型;只有在需要压测、需要断网环境、需要控制数据不出域的时候,才把模型拉到本地。这样迭代速度最快,也不会被显存卡住。
基础环境建议统一到 Python 3.10 或 3.11,虚拟环境用 venv 或 conda 都行,关键是每个项目独立,别把不同项目的依赖混在一起。核心依赖不多:模型 SDK、HTTP 客户端、参数校验库、Web 框架、评测用的数据处理库。下面是一份通用安装模板,版本号不写死,按你实际环境锁定即可。
# 创建并激活虚拟环境 python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate # 安装通用依赖(版本按项目锁文件为准) pip install -U openai pydantic httpx fastapi uvicorn python-dotenv # 可选:本地推理与向量检索 pip install -U sentence-transformers faiss-cpu目录结构建议一开始就分好,别等到文件堆成垃圾场再重构。输入素材、Prompt 模板、工具实现、评测任务集、运行日志、输出结果,这六类东西生命周期完全不同,混在一个目录里后期排错会非常痛苦。
agent-project/ ├── configs/ # 模型配置、工具开关、超时参数 ├── prompts/ # 系统提示词、子任务提示词模板 ├── tools/ # 每个工具一个文件,含 schema 与实现 ├── core/ # agent loop、记忆、规划器 ├── eval/ │ ├── tasks.jsonl # 评测任务集 │ ├── graders/ # 判分器 │ └── reports/ # 跑批结果 ├── logs/ # 轨迹日志,按 run_id 分目录 └── app.py # 服务入口配置文件里只放不会频繁变的东西,模型名、温度、最大轮数、单步超时、Token 预算上限,这些集中管理,方便做 A/B 对比。特别提醒一点:把所有跟模型相关的参数放进配置而不是硬编码在代码里,因为你要做评测对比时,唯一变量原则非常重要,改一个参数跑一次,才能知道是谁带来的提升。
{ "model": "your-model-name", "base_url": "http://127.0.0.1:8000/v1", "temperature": 0.2, "max_steps": 12, "step_timeout_sec": 60, "max_context_tokens": 24000, "tool_retry": 1, "log_dir": "./logs" }max_steps这个参数一定要设。没有步数上限的 Agent 是灾难,遇到死循环会一直烧 Token,跑批的时候一晚上能把预算跑光。step_timeout_sec同理,某个工具卡住不返回时,超时中断比无限等待优雅得多。这两个参数看着不起眼,是新手和老手最明显的分水岭之一。
4. 手写 Agent Loop:从零跑通一个最小智能体
这一节是整篇文章的核心。不管后面用什么框架,这个循环你必须能默写出来。逻辑只有四步:拼消息、调模型、判断是否有工具调用、执行工具并把结果塞回消息列表。终止条件也很简单,模型返回纯文本答案,或者达到最大轮数,或者触发人工中断。
先看一个不依赖任何编排框架的最小实现。它用的是 OpenAI 兼容的接口格式,绝大多数本地推理服务和云端服务都支持这套协议,把base_url和model换成你自己的即可。
# core/agent_loop.py import json import time from typing import Any from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY") def run_agent( user_input: str, tools_schema: list[dict], tool_impl: dict[str, Any], system_prompt: str = "你是一个严谨的助手,必要时调用工具,不要臆造事实。", max_steps: int = 12, ) -> dict: messages: list[dict] = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input}, ] trace: list[dict] = [] for step in range(max_steps): started = time.time() resp = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools_schema if tools_schema else None, temperature=0.2, ) msg = resp.choices[0].message trace.append({ "step": step, "latency_ms": int((time.time() - started) * 1000), "content": msg.content, "tool_calls": [tc.model_dump() for tc in (msg.tool_calls or [])], }) # 没有工具调用 = 模型给出最终答案,循环结束 if not msg.tool_calls: return {"answer": msg.content, "trace": trace, "steps": step + 1} # 把模型这一轮的决策写回历史 messages.append({ "role": "assistant", "content": msg.content or "", "tool_calls": [tc.model_dump() for tc in msg.tool_calls], }) # 逐个执行工具,结果以 tool 角色回填 for tc in msg.tool_calls: name = tc.function.name raw_args = tc.function.arguments or "{}" try: args = json.loads(raw_args) if name not in tool_impl: raise KeyError(f"未注册的工具: {name}") result = tool_impl[name](**args) content = json.dumps(result, ensure_ascii=False) except Exception as exc: # 关键:错误要回传给模型,而不是直接抛出 content = json.dumps({"error": str(exc)}, ensure_ascii=False) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": content, }) return {"answer": None, "trace": trace, "steps": max_steps, "reason": "max_steps_reached"}这段代码有几个细节值得反复看。第一,异常必须被捕获并以字符串形式回传给模型,这是 Agent 自愈能力的基础。你把异常直接抛出去,服务就挂了;回传给模型,模型有机会换个参数重试或者换条路走。第二,每条工具结果都要带tool_call_id,这是协议要求,丢了模型会报错。第三,trace 一定要记,它是你后面做评测、做复盘、做面试展示的唯一凭证,没有轨迹的 Agent 等于黑盒。
判断这个循环跑通的标准很简单:给它一个必须用工具才能回答的问题,比如“查询当前目录下有多少个 markdown 文件”,看它是否先调用列表工具再给答案。如果模型直接编一个数字出来,说明系统提示词约束不够,或者工具描述写得不够清楚。常见失败原因有三个:工具 schema 里description太含糊、模型本身指令遵循能力弱、温度设太高导致不稳定。
5. 工具调用与 Function Calling 实战
工具定义写得好不好,直接决定 Agent 的上限。很多人写 schema 只写个名字和参数类型,模型根本不知道什么时候该用,于是要么不调用,要么乱调用。写 tool 描述的原则是:说清楚它做什么、说清楚什么时候用、说清楚参数格式,最好再给一个反例,说明什么情况下不要用。
# tools/file_tools.py import os SAFE_ROOT = os.path.abspath("./workspace") def list_files(path: str = ".", pattern: str = "") -> dict: """列出指定目录下的文件。path 必须是 SAFE_ROOT 的子路径。""" target = os.path.abspath(os.path.join(SAFE_ROOT, path)) if not target.startswith(SAFE_ROOT): raise PermissionError("路径越界,拒绝访问") if not os.path.isdir(target): raise NotADirectoryError(f"不是目录: {path}") items = [] for name in sorted(os.listdir(target)): if pattern and pattern not in name: continue full = os.path.join(target, name) items.append({"name": name, "is_dir": os.path.isdir(full), "size": os.path.getsize(full) if os.path.isfile(full) else None}) return {"count": len(items), "items": items[:50]} LIST_FILES_SCHEMA = { "type": "function", "function": { "name": "list_files", "description": ( "列出指定目录下的文件和子目录。" "适用场景:用户询问某个目录里有什么、有多少文件、文件大小。" "不适用:读取文件内容(请用 read_file)、搜索文件内容(请用 grep_text)。" "path 只能是相对路径,且必须位于工作区内。" ), "parameters": { "type": "object", "properties": { "path": {"type": "string", "description": "相对路径,默认当前目录"}, "pattern": {"type": "string", "description": "文件名包含的关键字,可留空"}, }, "required": [], }, }, }这里有两个工程上的硬要求。第一,路径校验必须写在工具内部,不能指望模型遵守提示词。模型被骗的成本很低,你把../../etc这类输入交给它,迟早会有人试。第二,返回结果要有上限,items[:50]就是限制,不然一个大目录塞进上下文,几万 Token 下去后面的推理全废了。这类“返回截断 + 分页提示”的设计,是 Agent 工具工程里最容易被忽略的细节。
参数校验建议用 Pydantic 再包一层。模型生成的 JSON 经常出现类型错误,比如该传数组传成字符串、该传数字传成 “3”。用 Pydantic 做一层强校验,校验失败就把错误信息当成工具结果回传,模型通常下一轮就能改对。这套“校验失败 → 错误回传 → 模型自修正”的机制,是提升 Agent 稳定性的性价比最高的一招,比换更大的模型还管用。
工具数量也要控制。一次性暴露二三十个工具,模型的选错率会明显上升。经验做法是分组:主控 Agent 只拿五到八个高频工具,其余工具通过子 Agent 或者“工具检索”的方式按需挂载。如果你的项目工具特别多,可以先用一次轻量的模型调用做工具路由,把候选集缩到三五个再进主循环,这样既能保住能力,又能压住错误率。
6. 记忆、规划与深度任务编排
上下文管理是 Agent 从 Demo 走向可用的第二道坎。对话一长,消息列表就会膨胀,成本上升、延迟上升,更麻烦的是模型注意力被稀释,前面说过的约束记不住了。解决办法不是简单地砍历史,而是分层:最近几轮原文保留,较早的对话做摘要压缩,关键事实抽成结构化状态单独存,长期知识放向量库按需检索。
摘要压缩的触发时机要看 Token 预算而不是轮数。下面这段是通用模板,接入你实际的 tokenizer 即可,核心逻辑是“超过阈值就压,压完要验证关键信息没丢”。
# core/memory.py def maybe_compress(messages, count_tokens, threshold=18000, keep_recent=6): """超出预算时,把中间历史压成摘要,保留系统提示与最近若干轮。""" total = count_tokens(messages) if total <= threshold or len(messages) <= keep_recent + 2: return messages, None system_part = [m for m in messages if m["role"] == "system"] recent = messages[-keep_recent:] middle = [m for m in messages if m["role"] != "system" and m not in recent] # 这里的摘要调用建议用小模型或低温度,保证稳定且便宜 summary_prompt = ( "把下面的对话压缩成要点列表,保留:用户目标、已确认的事实、" "已失败的方法、待办事项。不要编造,不要省略数字和路径。\n\n" + "\n".join(f"{m['role']}: {m.get('content') or ''}" for m in middle) ) summary = call_small_model(summary_prompt) compressed = system_part + [ {"role": "system", "content": "历史对话摘要如下:\n" + summary} ] + recent return compressed, summary规划这块,ReAct 和 Plan-and-Execute 是必答的两种范式。ReAct 是边想边做,每步根据观察结果调整,适合步骤不确定、需要探索的任务;Plan-and-Execute 是先出完整计划再逐步执行,适合步骤清晰、可以并行的任务。前者灵活但容易跑偏,后者高效但计划一旦出错就全盘皆输,所以实践中常见的是混合:先出粗略计划,执行中允许重规划,每个子任务内部用 ReAct。
深度任务编排的关键在于把长链路切成短链路。一个二十步的任务如果一次性交给模型,中间任何一步幻觉都会传染;切成五个四步的子任务,每个子任务独立验收,出错只回滚一段。这就是 DeepAgent 这类思路的价值所在:它不追求单步能力提升,而是通过任务分解、中间产物检查点、失败重试,把累积误差压下来。工程上你可以用一个简单状态机来实现,每个子任务有明确的输入契约和输出契约,验收不过就重跑该子任务,而不是整个流程重来。
长期记忆要谨慎。向量库检索看起来很美,实际很容易引入无关内容污染上下文。建议只在两类场景开:一是用户画像、偏好这类跨会话稳定信息;二是知识库问答这类有明确来源的检索。写入策略上,不要每轮都写,让模型输出一个明确的“值得记住”的判断再落库,同时在检索时带上相似度阈值,低于阈值宁可不召回。
7. Harness 评测体系:把 Agent 当软件来测
这是拉开差距的一章。绝大多数人做 Agent 的做法是改一版提示词,手动试五六个输入,感觉不错就上线。这种做法的问题在于不可复现:你不知道这次提升是提示词带来的还是模型更新带来的,也不知道改完之后哪些原本能过的 case 挂了。Harness 的存在就是为了解决这个问题,它把任务集、运行器、判分器、报告四件套固定下来,每次改动跑一遍,用通过率说话。
任务集的构造要点是覆盖面而不是数量。一个实用的起步规模是五十到一百条,按能力维度分层:单工具任务、多工具串联、需要拒答的任务、需要多轮澄清的任务、容易触发幻觉的任务、边界输入(空值、超长、特殊字符)。每条任务写清输入、期望结果类型、判分方式。别一开始就追求几千条,先把判分逻辑跑顺。
{ "task_id": "single_tool_001", "category": "single_tool", "input": "工作区里有多少个 markdown 文件?", "expect": { "must_call_tools": ["list_files"], "answer_contains": ["个"], "max_steps": 4 }, "grader": "rule" }运行器的核心是不让一条失败拖垮整批。要设置单条超时、并发上限、失败重试次数,并且把每条任务的完整轨迹落盘,方便事后回看是哪一步跑偏的。下面是一个可复用的跑批模板,agent_fn就是前面那个循环包一层。
# eval/run_harness.py import json, time, traceback from concurrent.futures import ThreadPoolExecutor, as_completed def load_tasks(path="./eval/tasks.jsonl"): with open(path, encoding="utf-8") as f: return [json.loads(line) for line in f if line.strip()] def grade(task, result): exp = task.get("expect", {}) called = {tc["function"]["name"] for step in result.get("trace", []) for tc in (step.get("tool_calls") or [])} missing = set(exp.get("must_call_tools", [])) - called answer = result.get("answer") or "" hits = [kw for kw in exp.get("answer_contains", []) if kw in answer] passed = (not missing) and len(hits) == len(exp.get("answer_contains", [])) return {"passed": passed, "missing_tools": list(missing), "answer_matched": hits, "steps": result.get("steps")} def run_all(tasks, agent_fn, concurrency=4, retry=1): report = [] def one(t): last_err = None for _ in range(retry + 1): try: res = agent_fn(t["input"]) return {"task_id": t["task_id"], **grade(t, res), "error": None} except Exception as exc: last_err = traceback.format_exc(limit=2) return {"task_id": t["task_id"], "passed": False, "error": str(last_err)} with ThreadPoolExecutor(max_workers=concurrency) as pool: futures = {pool.submit(one, t): t for t in tasks} for fut in as_completed(futures): report.append(fut.result()) total = len(report) passed = sum(1 for r in report if r["passed"]) return {"total": total, "passed": passed, "pass_rate": round(passed / total, 4) if total else 0, "details": report} if __name__ == "__main__": tasks = load_tasks() result = run_all(tasks, lambda q: {"answer": "占位", "trace": []}) print(json.dumps({k: v for k, v in result.items() if k != "details"}, ensure_ascii=False, indent=2))规则判分能覆盖六七成场景,剩下的开放性问题需要 LLM 判分。用模型判分要注意三点:一是判分提示词要把评分标准和输出格式写死,只输出通过或不通过加理由;二是判分模型最好和被测模型不同源,避免自我偏袒;三是要定期人工抽查判分结果,校准漂移。判分的稳定性比判分的精细度更重要,一个总是给出模棱两可结论的判分器,会让整个评测体系失去意义。
跑出报告之后要形成习惯:每次改动都记一条记录,包含改动内容、通过率、平均步数、平均 Token 消耗、P95 延迟。这几项一起看,才能避免“通过率涨了但成本翻三倍”这种隐性退化。面试时能拿出这样一份曲线,比说十句“我对 Agent 有深入理解”有说服力。
8. 接口服务化、批量任务与资源成本观察
Agent 最终要接进业务,就得服务化。最常见的形态是一个 HTTP 接口,接收用户输入,内部跑 Agent 循环,把过程以流式方式吐出来。流式很重要,因为 Agent 一次任务可能要几十秒,没有中间反馈用户会以为卡死了。下面是一个最小可运行的 FastAPI 骨架,重点看超时控制、并发限流和会话隔离这三块。
# app.py import asyncio, uuid from fastapi import FastAPI, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel, Field app = FastAPI(title="Agent Service") SEMAPHORE = asyncio.Semaphore(4) # 全局并发上限,按显存/额度调整 class AskRequest(BaseModel): query: str = Field(min_length=1, max_length=4000) session_id: str | None = None max_steps: int = Field(default=10, ge=1, le=20) @app.post("/v1/agent/run") async def run(req: AskRequest): session_id = req.session_id or uuid.uuid4().hex if SEMAPHORE.locked() and SEMAPHORE._value == 0: raise HTTPException(status_code=429, detail="服务繁忙,请稍后重试") async with SEMAPHORE: try: result = await asyncio.wait_for( asyncio.to_thread(run_agent, req.query, [], {}, max_steps=req.max_steps), timeout=180, ) except asyncio.TimeoutError: raise HTTPException(status_code=504, detail="任务超时,请缩小问题范围") return {"session_id": session_id, **result} @app.get("/v1/agent/stream") async def stream(q: str): async def gen(): for chunk in ["正在分析问题...", "正在调用工具...", "已生成答案"]: yield f"data: {chunk}\n\n" await asyncio.sleep(0.3) return StreamingResponse(gen(), media_type="text/event-stream")部署命令很普通,关键是别把调试模式开到生产:
# 开发调试 uvicorn app:app --host 127.0.0.1 --port 7860 --reload # 生产启动:关闭 reload,限制 worker 数,避免显存被多进程打爆 uvicorn app:app --host 0.0.0.0 --port 7860 --workers 1 --timeout-keep-alive 75批量任务和在线接口要分开部署。批量场景的特点是允许排队、允许慢、但要求不丢任务,所以要用队列加状态落盘,而不是简单并发。每条任务记录task_id、状态、重试次数、最后一次错误,进程重启后能从断点续跑。并发数不要拍脑袋定,先测单条任务的平均耗时和峰值显存,再倒推能承受的并发,超过了就排队,别硬扛。
成本观察要落到指标上。至少盯这几个:单任务平均 Token 消耗、平均工具调用次数、P95 延迟、失败率、超时率、人均重试次数。工具调用次数是个很灵敏的指标,它突然上涨通常意味着提示词约束松了或者工具描述不清楚,模型在瞎试。Token 消耗突增则常见于上下文压缩失效或者工具返回结果没做截断。把这些指标接到监控上,比事后翻日志高效得多。
9. 常见问题与排查方法
Agent 的坑集中在几类:跑不通、跑不稳、跑不准、跑太贵。下面这张表覆盖了实际开发中最常撞见的问题,按现象去找原因,基本能定位到七八成。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型从不调用工具 | 工具描述含糊、提示词没约束 | 打印请求体,检查 tools 是否传空 | 补全 description,明确使用场景 |
| 工具参数解析失败 | 模型输出非法 JSON | 查看 trace 里的 raw arguments | 加 Pydantic 校验并回传错误 |
| Agent 陷入死循环 | 无步数上限、工具返回不明确 | 看 trace 里重复的调用序列 | 设 max_steps,工具返回加提示 |
| 长对话后答非所问 | 上下文超限、关键信息被挤出 | 统计每轮 Token 数 | 摘要压缩加结构化状态 |
| 同一问题结果不稳定 | 温度过高、工具结果非确定性 | 同输入跑十次对比 | 降温度,工具结果排序去重 |
| 接口 504 超时 | 步数过多、单步工具慢 | 看每步 latency 分布 | 拆分任务,加单步超时 |
| 并发上来就崩 | 显存不足、无并发限制 | 看显存曲线和 429 比例 | 加信号量,超限返回排队 |
| 批量任务卡住不动 | 队列阻塞、异常未捕获 | 查任务状态表和日志 | 加任务级超时和失败重试 |
| 输出内容敏感或越权 | 无内容校验、权限过宽 | 审计工具权限和输出 | 最小权限加人工确认环节 |
| 评测通过率波动大 | 任务集小、判分器不稳 | 重复跑同一版本对比 | 扩任务集,固化判分标准 |
几点补充经验。依赖安装失败大多不是代码问题,而是 Python 版本和 wheel 不匹配,尤其是涉及编译扩展的包,优先用预编译版本或者换 Python 3.10。模型服务连不上时,先用一条最简单的 curl 测通,别在 Agent 代码里猜,网络层和业务层的问题要分开定位。显存不足的表现通常是服务启动就退出或者推理中途 OOM,这时候降上下文长度比降精度更有效,因为显存占用跟序列长度是平方关系。
还有一个经常被忽略的问题:进程残留。反复重启服务之后,端口被占、显存没释放、GPU 上还挂着僵尸进程,表现就是“明明代码没改,突然跑不动了”。养成习惯,重启前先查端口占用和 GPU 进程,确认干净再起。这类环境问题排查起来最费时间,但预防成本极低。
10. 学习路线与最佳实践
把这条路线压缩成可执行的顺序。第一周不碰任何框架,纯手写 Agent Loop,把工具调用、错误回传、步数上限、日志落盘四件事做扎实,跑通三到五个真实任务。第二周加记忆和规划,做上下文压缩,试一次任务分解,对比分解前后的成功率。第三周搭 Harness,凑五十条任务集,把规则判分跑起来,形成第一份基线报告。第四周做服务化和批量,测并发、测超时、测成本,把指标接上监控。四周下来,你手里会有一个能演示、能对比、能讲清楚取舍的项目,这比刷完十套教程有用得多。
工程实践上有几条值得写进习惯里。一是永远先小参数验证,新功能先用两三步、短输入跑通,再放开长任务。二是保留一套最小可运行配置,模型、超时、工具开关都记在配置里,出问题时能快速回退到已知可用状态。三是输入、输出、日志分目录管理,按run_id归档,出问题能精确定位到某一次运行。四是批量任务必须带幂等键,重试不会产生重复副作用。五是接口服务要限制访问范围,加认证和限流,别把带工具权限的 Agent 裸露在公网上。
最后落在合规上。Agent 的能力越强,责任边界越要清楚:能读文件的要限定目录,能执行命令的要放进沙箱,能发请求的要走白名单,能生成内容的要有审核。涉及他人肖像、声音、作品的素材,必须拿到明确授权再处理;涉及个人信息的场景,日志脱敏、最小化采集、明确留存期限。这些不是给流程加负担,而是让系统能长期跑下去的前提。
如果只能先做一件事,建议从手写那个循环开始,加上完整的轨迹日志。有了轨迹,你就有了复盘和评测的基础,后面所有优化才有对比的基准;没有轨迹,再多的调参都只是碰运气。把这一层打牢,再去看各种编排框架和深度任务方案,你会发现它们解决的其实都是你已经遇到过的问题。