214. 技术:HarnessX 第 0 课:先让 hx 命令跑起来、Agent 的第一性原理其实就一个 HTTP 接口

2026.06.22

·技术harnessx

重点

  • 第 0 课真正要看懂的是这条链路:
    • 用户敲 hx ask "你是谁"
    • Node.js 收到命令参数。
    • 代码把参数变成 messages
    • fetch() 发一个 HTTP POST 请求给 DeepSeek。
    • DeepSeek 返回 JSON。
    • 代码从 JSON 里取出 assistant 的文本。
  • Agent / Harness 往后会变复杂,但第一性原理很简单:
    • 本质就是一个程序。
    • 程序把上下文组织成 HTTP 请求。
    • 大模型根据请求返回下一句话。
    • 程序再决定把这句话显示出来,还是继续做别的事。

一眼看懂

214. 技术:HarnessX 第 0 课:先让 hx 命令跑起来、Agent 的第一性原理其实就一个 HTTP 接口 图表 1

  • 当前课程的关键判断:
    • hx 是触发程序的入口。
    • messages 是发给模型的上下文。
    • fetch() 是程序和大模型之间的一来一回。

流程图

① 从命令到请求

214. 技术:HarnessX 第 0 课:先让 hx 命令跑起来、Agent 的第一性原理其实就一个 HTTP 接口 图表 2

② HTTP 一来一回

214. 技术:HarnessX 第 0 课:先让 hx 命令跑起来、Agent 的第一性原理其实就一个 HTTP 接口 图表 3

③ 数据形状怎么变

214. 技术:HarnessX 第 0 课:先让 hx 命令跑起来、Agent 的第一性原理其实就一个 HTTP 接口 图表 4

④ 多轮对话的一来一回

214. 技术:HarnessX 第 0 课:先让 hx 命令跑起来、Agent 的第一性原理其实就一个 HTTP 接口 图表 5

入口

先看命令怎么连到文件。

json
{
  "bin": {
    "hx": "./src/index.js"
  }
}
  • 这段配置只解决一件事:
    • 用户敲 hx
    • npm 找到 src/index.js
    • Node.js 执行这个文件。

文件第一行写:

js
`#!/usr/bin/env` node

// 这行告诉系统:
// 当前文件要交给 node 执行。

真正的参数入口是:

js
runCli(process.argv.slice(2));

// 用户输入:
// hx ask "你是谁"
//
// process.argv 大概是:
// [
//   "/path/to/node",
//   "/path/to/hx",
//   "ask",
//   "你是谁"
// ]
//
// slice(2) 之后,业务代码只看到:
// ["ask", "你是谁"]

命令分发

runCli() 做第一层分发。

js
async function runCli(argv) {
  const [command, ...rest] = argv;

  if (command === "hello") {
    console.log("HarnessX CLI is running.");
    return;
  }

  if (command === "ask") {
    // rest 是 ["你是谁"]
    // join 之后得到真正要发给模型的 prompt。
    const prompt = rest.join(" ").trim();
    await askOnce(prompt);
    return;
  }

  if (command === "chat") {
    // chat 会在一个进程里持续维护 messages。
    await chat(rest.join(" ").trim());
    return;
  }
}
  • 这里要记住:
    • hello 只验证入口。
    • ask 做一次模型请求。
    • chat 做多轮模型请求。

发出去的是什么

askOnce() 先把用户输入变成 messages

js
async function askOnce(prompt) {
  const messages = [
    systemMessage(),
    { role: "user", content: prompt },
  ];

  const answer = await callDeepSeek(messages);
  console.log(answer);
}

假设用户输入:

bash
hx ask "你是谁"

那发给 DeepSeek 的核心请求体就是:

json
{
  "model": "deepseek-chat",
  "messages": [
    {
      "role": "system",
      "content": "你是 HarnessX 第 0 课的对话助手。\n用中文回答,保持简短具体。\n当前只验证 Node.js CLI 入口和 DeepSeek 多轮对话。"
    },
    {
      "role": "user",
      "content": "你是谁"
    }
  ],
  "stream": false
}
  • 这就是最关键的东西:
    • model
      • 告诉服务端调用哪个模型。
    • messages
      • 告诉模型当前对话上下文。
    • stream: false
      • 这次先等完整回答返回。

代码里对应这一段。

js
const requestBody = {
  model,
  messages,
  stream: false,
};

怎么发出去

真正发出去的是 fetch()

js
const response = await fetch("https://api.deepseek.com/chat/completions", {
  method: "POST",
  headers: {
    // 告诉服务端:我发的是 JSON。
    "Content-Type": "application/json",

    // 告诉服务端:我是谁,我有没有权限调用。
    Authorization: `Bearer ${apiKey}`,
  },

  // HTTP 请求体必须是字符串,所以要 JSON.stringify。
  body: JSON.stringify(requestBody),
});
  • 这段代码说明 Agent 的底层动作:
    • 程序组织上下文。
    • 程序带上 API Key。
    • 程序向一个 URL 发 HTTP POST。

如果要用更朴素的话讲:

text
我把聊天记录整理成 JSON,
带上 API Key,
POST 到 DeepSeek 的接口,
等它返回一个 JSON。

返回的是什么

DeepSeek 返回的是一个 JSON。

大概长这样:

json
{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "我是 HarnessX 第 0 课的对话助手。"
      }
    }
  ]
}

当前代码只取这一段:

js
const payload = await response.json();
const content = payload?.choices?.[0]?.message?.content;
return content;
  • 这里要记住:
    • response 是 HTTP 响应。
    • payload 是响应 JSON。
    • choices[0].message 是模型给出的第一条回复。
    • content 是最终要打印给用户看的文本。

多轮对话

多轮对话的本质是:每次请求都带上更长的 messages

第一轮:

json
[
  { "role": "system", "content": "你是 HarnessX 第 0 课的对话助手。" },
  { "role": "user", "content": "我的名字是 liguwe" }
]

模型回答后,代码把 assistant 也追加进去。

json
[
  { "role": "system", "content": "你是 HarnessX 第 0 课的对话助手。" },
  { "role": "user", "content": "我的名字是 liguwe" },
  { "role": "assistant", "content": "你好,liguwe。" }
]

第二轮用户再问:

json
[
  { "role": "system", "content": "你是 HarnessX 第 0 课的对话助手。" },
  { "role": "user", "content": "我的名字是 liguwe" },
  { "role": "assistant", "content": "你好,liguwe。" },
  { "role": "user", "content": "我刚才说我的名字是什么?" }
]

模型能答出来的原因,是代码每次都把历史消息重新发给它。

这段代码就是关键:

js
// 用户说一句,追加到 messages。
messages.push({ role: "user", content: text });

// 把完整 messages 发给模型。
const answer = await callDeepSeek(messages);

// 模型回一句,也追加到 messages。
messages.push({ role: "assistant", content: answer });

怎么跑

先验证入口。

bash
npm link
hx --help
hx hello

看到这个输出,说明 CLI 入口通了。

text
HarnessX CLI is running.

再配置模型。

bash
cp .env.example .env

.env 里填:

env
DEEPSEEK_API_KEY=你的 key
DEEPSEEK_MODEL=deepseek-chat

跑一次 HTTP 调用。

bash
hx ask "只回复 OK"

预期看到:

text
OK

跑多轮对话。

bash
hx chat

输入:

text
hx> 我的名字是 liguwe
hx> 我刚才说我的名字是什么?
  • 如果第二句能回答出来:
    • 说明 messages 被持续追加。
    • 说明每次 HTTP 请求都带上了上下文。

这一课真正要记住

  • hx
    • 只是触发 Node.js 程序的入口。
  • process.argv
    • 把用户命令变成 JS 数组。
  • messages
    • 把用户输入和历史上下文组织成模型能读的格式。
  • fetch()
    • messages 作为 HTTP 请求体发给 DeepSeek。
  • payload.choices[0].message.content
    • 从 HTTP 响应里取出模型回答。

第 0 课最终要讲清楚的是这句话:

text
Agent 的起点,就是程序把上下文打包成 HTTP 请求发给大模型,
再从 HTTP 响应里取出模型的下一句话。