最近在折腾 Agent 项目发现圈子里讨论最多的几个词就是 DeepSeek-Harness、harness anything、skill 编排。踩了一圈坑之后我最大的体会是搭 Agent 开发环境难的不是装框架而是怎么让环境可靠——可复现、可调试、可回归。这篇文章我想用一个大家最熟悉的业务模块——Android 登录模块完整走一遍用 Harness 搭 Agent 开发环境的流程。读完你能直接照着搭出自己的一套环境并且学到我实测下来最有价值的那些细节。这套方案适合这几类人准备把 Agent 落到真实业务里的后端/客户端工程师被各种 Agent 框架绕晕想找轻量方案的开发者以及想在团队里推广环境即代码的工程负责人。我会尽量用大白话讲清楚每个环节为什么这么做而不是只丢一堆配置。1. Agent 开发环境为什么需要框架先搞清楚 Harness 的定位1.1 从裸调大模型到带工具箱的 Agent先回想一下最原始的调模型写代码方式你拿着 API Key写一段 Python 脚本把用户的输入拼进 Prompt等模型吐一段 JSON 回来。这种方式做个小 Demo 没问题但一旦业务逻辑复杂起来立刻会遇到三个坎。第一是大模型本身没有记忆每次对话都要重新把上下文全塞进去第二是大模型不会主动调用外部系统你想让它帮你发个验证码、查个订单它只能口嗨第三是输出格式不稳定模型今天返回这种 JSON明天可能换个结构你的业务代码跟着遭殃。Agent 框架要解决的就是这三件事管理会话上下文、提供工具调用能力、把模型输出约束成可靠结构。Harness 这类轻量级框架做的正是这件事而且它做得比大型全家桶更克制——不绑架你的项目结构只给你 Agent 运行时最核心的骨架。1.2 Harness 想解决的几个核心问题社区里叫 Harness 的项目不止一个但设计理念高度一致让大模型在一个受控环境里安全地调用工具、完成任务。我理解它的核心抽象就四个Skill技能、Tool工具、Runtime运行时、Session会话。Skill 是一组能力的封装比如发送短信验证码是一个技能校验验证码是另一个技能Tool 是 Skill 落地成可执行函数后的最小单元对应你代码里一个具体的方法Runtime 负责调度模型和工具之间循环相当于 Agent 的心脏Session 则承载每一次对话的上下文。和 LangChain、CrewAI 这类重框架相比Harness 最大的特点是只做编排不做业务。它不会帮你封装登录逻辑也不会替你决定业务流程它只提供一个让模型思考-调用-观察-再思考的循环。这个循环越简单越透明出问题的时候越容易排查。1.3 为什么拿 Android 登录模块当第一个试点选试点业务是有讲究的。我当时列了几个候选登录模块、购物车模块、推荐流模块。最后选了登录原因有三个。第一登录模块边界极其清晰。一个手机号验证码登录流程就是发起验证码、校验验证码、创建会话、返回令牌。状态流转固定不会出现这个需求到底算不算登录这种模糊地带。第二登录模块有天然的确定性校验闭环。验证码是否正确、手机号格式是否合法这些都是非黑即白的判断。Agent 在这种场景里本来就不应该自由发挥它要做的只是按规则调用工具、把结果反馈给用户。这种确定性逻辑 LLM 决策的分界恰好是 Agent 工程化最理想的起点。第三登录模块牵涉安全和权限能逼你把环境的可靠性做扎实。你会自然地去考虑工具权限、日志脱敏、失败重试这些在生产环境里必须面对的问题而不是停留在玩具 Demo 层面。候选模块边界清晰度确定性校验工程挑战是否适合首试点登录模块高高中安全和权限非常适合购物车模块中中低一般推荐流模块低低高不建议2. 环境初始化搭一套能稳定复现的 Harness 开发环境2.1 前置依赖与版本选择先说结论我实测下来这套环境需要的依赖非常少Python 3.10 以上、git、uv 或者 poetry 任一即可Docker 可选。不需要单独的 Android 环境——这个要划重点我们这里的 Agent 开发环境跑在本地或者服务器上Android 登录模块在这里是以接口契约和 Mock 服务的形式出现的。你想连真机也可以但第一个版本用 Mock 接口完全够用并且能帮你排除掉一堆设备相关的干扰因素。Python 版本强烈建议锁定 3.10 或 3.11。我试过在 3.9 上装某个版本的 Harness依赖解析直接报错3.12 虽然能装上但个别原生依赖编译需要额外的构建工具链小白容易卡住。用 3.10/3.11踩坑成本最低。2.2 拉取 Harness 并安装安装过程我给你一个可以直接抄的流程。假设项目目录叫android-login-agentmkdir android-login-agent cd android-login-agent git clone https://github.com/deepseek-ai/DeepSeek-Harness.git harness cd harness uv sync --extra dev harness --version这里有两个容易踩的坑。第一uv 同步依赖的时候尽量用--extra dev否则后面跑测试和调试工具的时候会报缺模块第二不建议直接pip install到全局环境一个 Agent 项目通常要配多个版本的依赖用 uv 或者 poetry 隔离环境能帮你省掉后面大量为什么我这里跑不起来的困扰。装完框架之后需要配置模型供应商。Harness 默认支持 OpenAI 兼容协议所以接 DeepSeek 只需要把 API Key 写进环境变量export DEEPSEEK_API_KEYsk-xxxx export HARNESS_MODELdeepseek-chat注意密钥永远不要写进项目文件。我见过不止一个同事把 key 直接贴在 config.yaml 里结果仓库一同步就等于公开了密钥。正确做法是写进.env文件并且把.env加入.gitignore。2.3 初始化工程骨架我的建议是不要一上来就铺一层很深的目录保持简单后续按需生长。一个跑通登录 Demo 的骨架差不多长这样android-login-agent/ ├── harness/ # Harness 框架本身 ├── skills/ │ ├── send_login_code/ # 发送验证码技能 │ ├── verify_login_code/ # 校验验证码技能 │ └── create_user_session/ # 创建会话技能 ├── mocks/ # 登录服务的 Mock 实现 ├── tests/ │ ├── fixtures/ # 测试固定数据 │ └── test_login_flow.py ├── agent.yaml # Agent 编排配置 ├── .env.example # 环境变量模板 └── pyproject.toml # 项目依赖骨架定好之后你需要建立两条铁律一是锁定版本。框架版本、模型版本、依赖版本都写死不然今天跑通明天挂掉因为你不知道哪个上游依赖偷偷变了行为二是 Mock 服务要跟真实接口契约一致。接口字段名、状态码、错误结构都要按真实 Android 端对接的协议来定义否则后面验收集成的时候会发现Agent 是对的是 Mock 跟真实服务长得不一样。3. 把 Android 登录模块拆成 Agent 能指挥的技能3.1 先把登录流程抽象成状态机动手写代码之前先把业务逻辑画清楚。手机号验证码登录看起来简单但它其实是一个典型的有限状态机初始状态 - 等待验证码 - 校验中 - 会话建立 - 已登录。任何一步失败都会回到初始状态或者进入异常分支。把这个状态机画出来有两个好处。第一它能帮你看清楚哪些环节是确定性的手机号格式校验、验证码比对、会话生成这些写死就行第二它能帮你定位 LLM 应该站在哪一层做决策模型负责听懂用户意图、选择合适的技能、在失败时决定下一步策略而不是去决定验证码到底对不对。3.2 Skill 与 Tool 的边界怎么切这是 Agent 工程化里最核心也最容易混乱的点。我的原则很简单涉及安全、状态变更、资金、隐私的操作全部做成确定性 Tool不给模型自由发挥的空间而需要理解意图、组合多步操作的流程分配给 LLM 决策。拿登录模块举例我最终切的 Tool 列表是这样技能名称底层 Tool确定性说明发送验证码send_login_code高手机号格式校验、频控、超时校验验证码verify_login_code高密文比对只返回成功/失败创建议题create_user_session高生成 token绑定 session查询登录状态get_login_status高给 Agent 一个确认当前状态的抓手细看你会发现所有 Tool 都是确定性执行、结构化返回的没有一个需要模型猜。模型要做的只是判断用户想登录、需要先发验证码、验证码对不对、下一步该调什么。3.3 实现一个验证码 Skill说了这么多上一个实际可用的代码片段。用 Harness 约定写一个校验验证码的 Skill# skills/verify_login_code/skill.py from pydantic import BaseModel, Field import harness class VerifyLoginCodeInput(BaseModel): phone: str Field(description用户手机号) code: str Field(description短信中的验证码) request_id: str Field(description发送验证码时返回的请求标识) harness.tool(nameverify_login_code, description校验短信验证码是否正确) def verify_login_code(input_data: VerifyLoginCodeInput) - dict: # 真实项目里这里改成 auth-service 的 HTTP 调用 # 这里只写核心逻辑防止验证码被重复使用 stored mock_store.get(input_data.request_id) if stored is None: return {ok: False, error: request_id 不存在或已过期} if stored.verified: return {ok: False, error: 验证码已被使用请重新发送} if stored.code ! input_data.code: return {ok: False, error: 验证码错误} stored.verified True return {ok: True, phone: input_data.phone}这个实现看起来很简单但它埋了三层可靠性设计。第一验证码一次性。用verified标记保证同一个验证码只能用一次这是登录安全的基本要求。第二即使校验失败返回数据也是结构化 JSON模型能直接读懂并决定下一步动作不需要它去理解一堆含糊的报错。第三入参用 pydantic 做了强校验字段描述写清楚这样模型生成参数的时候更不容易出错。实操心得写 Skill 的时候每个返回字段都要考虑模型能不能看懂。宁可多返回一个 error 字段也别让模型去猜失败原因。我在早期版本里试过只返回{ok: false}结果模型反复重试同一个错误操作把验证码都耗光了。4. 选择模型与编排把技能串成一个能跑的 Agent4.1 注册工具与技能写好了 Skill接下来要把它们注册进 Agent。Harness 通常支持两种方式一种是装饰器自动发现另一种是在配置文件里显式声明。项目大了之后我推荐显式声明因为自动发现虽然省事但调试的时候你根本不知道哪些工具被加载进来了。我的agent.yaml长这样agent: name: android-login-agent model: deepseek-chat temperature: 0.0 system_prompt: | 你是 Android 登录助手负责帮用户完成手机号验证码登录。 严格按以下流程执行 1. 用户提供手机号后调用 send_login_code 发送验证码。 2. 用户提供验证码后调用 verify_login_code 校验。 3. 校验通过后调用 create_user_session 创建会话。 4. 任何一步失败直接返回错误原因不要重试超过一次。 tools: - send_login_code - verify_login_code - create_user_session - get_login_status session: ttl: 30m这里有几个参数是我的经验之谈。temperature: 0.0是必须的登录流程不需要任何创造性温度归零才能保证同样输入尽量同样输出。system_prompt里加了不要重试超过一次这能有效防止模型在验证码错误的时候无限循环调用。4.2 把校验结果作为硬约束回传编排环节最容易忽略的一点是Tool 返回结果必须成为后续决策的硬约束而不是参考信息。比如verify_login_code返回{ok: false, error: 验证码错误}模型的下一个动作就应该是告诉用户验证码错误并停下来而不是自作主张换个验证码再试。要做到这一点除了在系统提示词里写清楚还有一个更狠的办法在 Tool 返回里加retryable字段。当某个工具返回{ok: false, retryable: false}时运行时就该强制终止当前工具循环只有retryable: true才允许模型重新规划。这套机制相当于给模型装了一道刹车片。实测下来加上这个约束之后登录流程的失败重试次数直接降了一半多。4.3 跑通一次完整的登录闭环环境配好、技能注册完终于到了最激动人心的时刻——跑第一个完整流程。我在终端里执行命令看到这样的输出[00:00:00.124] user: 用 138****8888 登录一下 [00:00:00.873] llm: 用户提供了手机号先发送验证码 [00:00:00.912] tool: send_login_code(phone138****8888, channelsms) [00:00:01.401] tool: return {ok: true, request_id: req_2025_0001} [00:00:01.402] llm: 验证码已发送请用户查收 [00:00:04.211] user: 验证码是 482913 [00:00:04.890] llm: 调用 verify_login_code 校验 [00:00:04.923] tool: verify_login_code(request_idreq_2025_0001, code482913) [00:00:05.087] tool: return {ok: true, phone: 138****8888} [00:00:05.120] llm: 验证通过创建会话 [00:00:05.188] tool: create_user_session(phone138****8888) [00:00:05.402] tool: return {token: eyJhbGciOi..., expires_in: 604800} [00:00:05.410] agent: 登录成功会话已建立这个日志的价值在于你可以完整看到用户输入 - 模型决策 - 工具调用 - 结果返回 - 下一步决策的全过程。所谓 Agent 环境可靠本质就是这条链路上的每个环节都可观测、可回放、可复现。如果哪天用户反馈登录失败你只要把当次 Session 的日志拉出来就能准确指出是哪一步出了问题。5. 可靠性不只是不崩日志、回放、测试与容错5.1 全链路日志与 Trace很多 Agent 项目跑起来像黑盒出了问题只能干瞪眼。我的经验是从第一天起就要把链路日志做全。每个工具调用都要记录入参、出参、耗时、失败原因每次 LLM 请求都要记录模型、token 数、延时整个 Session 要有唯一的 trace id 串起来。具体落地时我会给每个工具加一个简单的装饰器统一做埋点import time import logging logger logging.getLogger(harness.trace) def traced_tool(func, name): def wrapper(*args, **kwargs): start time.time() try: result func(*args, **kwargs) logger.info(tool%s args%s result%s cost%.0fms, name, kwargs, result, (time.time() - start) * 1000) return result except Exception as exc: logger.error(tool%s args%s error%s, name, kwargs, exc) raise return wrapper有了这份日志排查问题的效率能提升一个量级。特别是当模型突然抽风调用了一个不存在的工具参数时你能清清楚楚看到它到底生成了什么。5.2 用 Mock 服务和固定种子做回归测试LLM 是概率模型Agent 环境最大的敌人就是这次能跑通下次跑不通。为了对抗这种不确定性我强烈建议把变量和不变量分开。变量的部分是模型的输出不变量的部分是工具的行为。工具的行为要固化——这正是 Mock 服务的主场。我做了两套 Mock一套是日常开发用的快乐路径 Mock所有调用都返回成功另一套是回归测试用的场景化 Mock固定返回预置数据比如verify_login_code永远返回验证码错误。配合固定种子环境跑回归测试时每次测试用的手机号、验证码都是固定的。这样当你修改某个 Skill 之后只要跑一遍测试套件就能确认没有破坏已有的登录流程。下面是回归测试的核心逻辑def test_login_flow_success(): result agent.run(用 138****8888 登录验证码是 482913) assert result[status] logged_in assert result[steps] [send_login_code, verify_login_code, create_user_session]这里只校验 Agent 完整走到了登录成功并校验了工具调用顺序。硬编码顺序可能会比较脆弱但对第一个版本来说明确固定比灵活更重要。5.3 常见问题与排查技巧实录在搭这套环境的过程中我踩过的坑和网上大家讨论最多的问题整理了一张速查表现象原因排查与解决模型反复调用同一个工具返回错误信息不够清晰或缺少 retryable 约束检查工具返回结构增加终止条件工具入参缺字段Skill 描述不清楚模型不知道要传 phone在 pydantic Field 里写清楚每个字段含义验证码被重复发送频控逻辑没放在 Tool 层发送验证码的 Skill 里必须做限频环境依赖总是冲突没有锁定版本用 uv/poetry 锁版本CI 里用同一锁文件日志脱敏不到位手机号、token 直接打进日志加统一的脱敏过滤器只保留前后三位这里重点说第一个问题。模型反复调用同一个工具往往不是模型笨而是你的返回信息不足以让它做决策。比如验证码错误你不能只返回{ok: false}而要返回{ok: false, retryable: false, error: 验证码错误请重新获取验证码}。模型看到明确的错误语义和下一步建议才会走正确分支。异常分支一定要实测。快乐路径跑通了不算完成验证码错误、会话过期、手机号格式错误这三个分支至少要覆盖一遍否则上线就是事故。5.4 安全与审计登录模块绕不开的责任登录模块碰的是账号体系安全问题不能等到上线前才想。我的建议是三件套权限最小化、日志脱敏、操作审计。权限最小化指的是 Agent 运行时只拥有它完成任务所需的最小权限。验证码 Skill 只允许读——不允许改手机号绑定关系会话创建 Skill 只允许生成 token——不允许修改用户资料。日志脱敏则是所有日志输出前手机号中间四位打码、token 截断。操作审计更进一步把 Agent 每次工具调用都落库保存调用者、时间、入参出参摘要出问题时能追溯。这三件事看起来繁琐但正因为目标是登录模块你才会被逼着把安全地基打牢。否则后面扩展到支付模块再补安全设计就晚了。6. 从登录到全模块这套环境的扩展路径6.1 沉淀可复用的模块模板登录模块跑通后最大的收获不是登录本身而是一套可以复用到其他模块的模板。我总结下来每个 Agent 化业务模块的模板包含五个部分模块状态机定义、工具清单与描述、Mock 服务契约、回归测试用例、环境配置文件。有了这个模板第二个模块——比如注册模块——的搭建成本会大幅下降。因为你只需要重新定义状态机和工具清单环境、测试框架、日志规范全是现成的。6.2 团队协作与迭代节奏环境即代码这件事一个人用是技术团队用是工程。我建议从第一天就引入版本管理和评审流程Skill 改动必须走 PR每次合并前自动跑一遍 Agent 回归测试模型升级、依赖升级单独做不能和业务改动混在一起。迭代节奏上我的经验是小步快跑单点收口。一次只改一个 Skill、增加一个工具跑通后立刻固化测试。这套环境我最看重的就是能让我对这个 Agent 的行为有很强的掌控感——每次改动都有测试兜底不会出现改了一个工具、炸了另一个流程的情况。我个人实际用下来的体会是搭 Harness Agent 开发环境真正难的不是框架本身而是你有没有一套约束不确定性的工程方法。把 LLM 的输出圈在一个小盒子里其余全部用确定性代码兜底这就是可靠性最实在的秘诀。用 Android 登录模块当第一个试点看起来只是选了个简单的业务但它逼着你把状态、校验、权限、日志、回归全部想清楚——而这些恰恰是任何一个 Agent 项目走向生产环境都绕不开的关卡。先把这个关卡打通后面接什么模块都会轻松很多。