文章

47 · 神经网络原理揭秘与本地 AI Agent 调度

#054 · 2026-04-17 · Python

🔗 知识图谱导航:阅读本文前,建议先回顾《31 · NumPy 高性能数组计算》里的矩阵运算,以及《46 · 模型调优:GridSearchCV、学习曲线与模型持久化》里的训练评估思路。本文会把这些基础推进到“手写神经网络 + 本地 AI Agent 调度”。 NexDo Time · 2026-04-17 · 预计阅读 28 分钟

痛点与架构

神经网络和 AI Agent 常被讲得很玄:一边是矩阵、梯度、激活函数,另一边是本地模型、接口、工具路由。新手真正卡住的地方通常不是“知道名词”,而是不知道数据到底从哪里进去、经过了谁、最后为什么能得到输出。

这一篇把复杂系统拆成两条线:第一条线用 XOR 小数据手写一个三层神经网络,让你看见前向传播、反向传播和损失下降;第二条线用 Ollama 的本地接口设计一个轻量 Agent,让你看懂“用户问题 -> 意图识别 -> 工具选择 -> 大模型回答”的调度流程。

输入数据/用户问题
      │
      ├─ 神经网络线:X -> W/b -> sigmoid -> loss -> backward -> predict
      │
      └─ Agent 线:query -> intent -> tool -> OllamaClient -> response

步步为营:核心逻辑自适应拆解

这一篇的知识密度较高,所以拆成 7 个小步骤。每个 Step 都先给出文末完整源码里的真实片段,再给出可以单独复制运行的演示代码。你不用一次吞下整篇源码,先把每一块小积木跑通,再回到文末看完整脚本。

Step 1:用 NeuralNetwork 看懂神经网络的矩阵流水线

痛点与机制

刚接触神经网络时,最容易被“层、权重、激活”这些词吓住。把它想成一条工厂流水线:输入 X 是原材料,W1/W2 是机器参数,b1/b2 是校准旋钮,sigmoid 是质检门,把任何数字压到 0 到 1 之间。这个 Step 先不训练,只看一次前向传播,让新手明确数据从 2 个输入特征流到 4 个隐藏神经元,再流到 1 个预测概率。

核心源码(逐字来自文末完整源码)

class NeuralNetwork:
    """3层全连接神经网络:输入层→隐藏层→输出层"""

    def __init__(self, input_dim: int, hidden_dim: int, output_dim: int,
                 lr: float = 0.1):
        rng = np.random.RandomState(42)
        self.W1 = rng.randn(input_dim, hidden_dim) * 0.5
        self.b1 = np.zeros((1, hidden_dim))
        self.W2 = rng.randn(hidden_dim, output_dim) * 0.5
        self.b2 = np.zeros((1, output_dim))
        self.lr = lr
        self.losses: List[float] = []

    @staticmethod
    def sigmoid(z: np.ndarray) -> np.ndarray:
        return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))

    @staticmethod
    def sigmoid_deriv(a: np.ndarray) -> np.ndarray:
        return a * (1.0 - a)

    def forward(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        Z1 = X @ self.W1 + self.b1
        A1 = self.sigmoid(Z1)
        Z2 = A1 @ self.W2 + self.b2
        A2 = self.sigmoid(Z2)
        return Z1, A1, Z2, A2

    def backward(self, X: np.ndarray, y: np.ndarray,
                 A1: np.ndarray, A2: np.ndarray) -> None:
        n = X.shape[0]
        delta2 = (A2 - y) * self.sigmoid_deriv(A2)
        dW2 = A1.T @ delta2 / n
        db2 = delta2.mean(axis=0, keepdims=True)
        delta1 = (delta2 @ self.W2.T) * self.sigmoid_deriv(A1)
        dW1 = X.T @ delta1 / n
        db1 = delta1.mean(axis=0, keepdims=True)
        self.W1 -= self.lr * dW1
        self.b1 -= self.lr * db1
        self.W2 -= self.lr * dW2
        self.b2 -= self.lr * db2

    def train(self, X: np.ndarray, y: np.ndarray, epochs: int = 10000) -> None:
        for _ in range(epochs):
            _, A1, _, A2 = self.forward(X)
            loss = float(-np.mean(y * np.log(A2 + 1e-9) + (1 - y) * np.log(1 - A2 + 1e-9)))
            self.losses.append(loss)
            self.backward(X, y, A1, A2)

    def predict(self, X: np.ndarray) -> np.ndarray:
        _, _, _, A2 = self.forward(X)
        return (A2 >= 0.5).astype(int)

可运行演示(补齐 Mock 数据与 print 反馈)

from typing import List, Tuple

import numpy as np

class NeuralNetwork:
    """3层全连接神经网络:输入层→隐藏层→输出层"""

    def __init__(self, input_dim: int, hidden_dim: int, output_dim: int,
                 lr: float = 0.1):
        rng = np.random.RandomState(42)
        self.W1 = rng.randn(input_dim, hidden_dim) * 0.5
        self.b1 = np.zeros((1, hidden_dim))
        self.W2 = rng.randn(hidden_dim, output_dim) * 0.5
        self.b2 = np.zeros((1, output_dim))
        self.lr = lr
        self.losses: List[float] = []

    @staticmethod
    def sigmoid(z: np.ndarray) -> np.ndarray:
        return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))

    @staticmethod
    def sigmoid_deriv(a: np.ndarray) -> np.ndarray:
        return a * (1.0 - a)

    def forward(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        Z1 = X @ self.W1 + self.b1
        A1 = self.sigmoid(Z1)
        Z2 = A1 @ self.W2 + self.b2
        A2 = self.sigmoid(Z2)
        return Z1, A1, Z2, A2

    def backward(self, X: np.ndarray, y: np.ndarray,
                 A1: np.ndarray, A2: np.ndarray) -> None:
        n = X.shape[0]
        delta2 = (A2 - y) * self.sigmoid_deriv(A2)
        dW2 = A1.T @ delta2 / n
        db2 = delta2.mean(axis=0, keepdims=True)
        delta1 = (delta2 @ self.W2.T) * self.sigmoid_deriv(A1)
        dW1 = X.T @ delta1 / n
        db1 = delta1.mean(axis=0, keepdims=True)
        self.W1 -= self.lr * dW1
        self.b1 -= self.lr * db1
        self.W2 -= self.lr * dW2
        self.b2 -= self.lr * db2

    def train(self, X: np.ndarray, y: np.ndarray, epochs: int = 10000) -> None:
        for _ in range(epochs):
            _, A1, _, A2 = self.forward(X)
            loss = float(-np.mean(y * np.log(A2 + 1e-9) + (1 - y) * np.log(1 - A2 + 1e-9)))
            self.losses.append(loss)
            self.backward(X, y, A1, A2)

    def predict(self, X: np.ndarray) -> np.ndarray:
        _, _, _, A2 = self.forward(X)
        return (A2 >= 0.5).astype(int)


X = np.array([[0, 1]], dtype=float)
network = NeuralNetwork(input_dim=2, hidden_dim=4, output_dim=1, lr=0.5)
Z1, A1, Z2, A2 = network.forward(X)

print("输入 X 形状:", X.shape)
print("第一层权重 W1 形状:", network.W1.shape)
print("隐藏层激活 A1 形状:", A1.shape)
print("输出层结果 A2:", np.round(A2, 4).tolist())
print("直觉:2 个输入特征 -> 4 个隐藏神经元 -> 1 个二分类概率")

Step 2:用 XOR 训练证明网络能学非线性关系

痛点与机制

XOR 像一个小型逻辑谜题:两个输入相同输出 0,不同输出 1,单条直线分不开。隐藏层就像在纸上多折了一次,把原本拧巴的数据摊平。这里训练 5000 轮后打印预测表和损失曲线,新手能看到“模型不是背概念,而是真的从错误里一点点调参数”。

核心源码(逐字来自文末完整源码)

def mode_xor() -> None:
    print(f"[{nexdo_time()}] XOR 神经网络训练")
    # XOR 数据集
    X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
    y = np.array([[0], [1], [1], [0]], dtype=float)

    nn = NeuralNetwork(input_dim=2, hidden_dim=4, output_dim=1, lr=1.0)
    nn.train(X, y, epochs=5000)

    # 预测结果
    preds = nn.predict(X)
    rows = []
    for xi, yi, pi in zip(X, y, preds):
        correct = "✓" if int(yi[0]) == int(pi[0]) else "✗"
        rows.append([f"[{int(xi[0])},{int(xi[1])}]", int(yi[0]), int(pi[0]), correct])
    print_table(["输入", "真实", "预测", "正确"], rows, "XOR 预测结果")

    # 损失曲线(ASCII)
    sampled = nn.losses[::500]
    max_loss = max(sampled)
    print("\n  训练损失曲线(每500轮)")
    height = 8
    for h in range(height, 0, -1):
        threshold = h * max_loss / height
        line = f"  {threshold:6.3f} │"
        for l in sampled:
            line += "█" if l >= threshold else " "
        print(line)
    print(f"         └{'─'*len(sampled)}")
    print(f"  最终损失: {nn.losses[-1]:.6f}  准确率: {(preds == y.astype(int)).mean():.0%}")

可运行演示(补齐 Mock 数据与 print 反馈)

import time
from typing import List, Tuple

import numpy as np

def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")

def print_table(headers: list, rows: list, title: str = "") -> None:
    if title:
        print(f"\n{'='*65}\n  {title}\n{'='*65}")
    col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
                  for i, h in enumerate(headers)]
    print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
    print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
    print(f"├{'┼'.join('─'*(w+2) for w in col_widths)}┤")
    for row in rows:
        print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
    print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")

class NeuralNetwork:
    """3层全连接神经网络:输入层→隐藏层→输出层"""

    def __init__(self, input_dim: int, hidden_dim: int, output_dim: int,
                 lr: float = 0.1):
        rng = np.random.RandomState(42)
        self.W1 = rng.randn(input_dim, hidden_dim) * 0.5
        self.b1 = np.zeros((1, hidden_dim))
        self.W2 = rng.randn(hidden_dim, output_dim) * 0.5
        self.b2 = np.zeros((1, output_dim))
        self.lr = lr
        self.losses: List[float] = []

    @staticmethod
    def sigmoid(z: np.ndarray) -> np.ndarray:
        return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))

    @staticmethod
    def sigmoid_deriv(a: np.ndarray) -> np.ndarray:
        return a * (1.0 - a)

    def forward(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        Z1 = X @ self.W1 + self.b1
        A1 = self.sigmoid(Z1)
        Z2 = A1 @ self.W2 + self.b2
        A2 = self.sigmoid(Z2)
        return Z1, A1, Z2, A2

    def backward(self, X: np.ndarray, y: np.ndarray,
                 A1: np.ndarray, A2: np.ndarray) -> None:
        n = X.shape[0]
        delta2 = (A2 - y) * self.sigmoid_deriv(A2)
        dW2 = A1.T @ delta2 / n
        db2 = delta2.mean(axis=0, keepdims=True)
        delta1 = (delta2 @ self.W2.T) * self.sigmoid_deriv(A1)
        dW1 = X.T @ delta1 / n
        db1 = delta1.mean(axis=0, keepdims=True)
        self.W1 -= self.lr * dW1
        self.b1 -= self.lr * db1
        self.W2 -= self.lr * dW2
        self.b2 -= self.lr * db2

    def train(self, X: np.ndarray, y: np.ndarray, epochs: int = 10000) -> None:
        for _ in range(epochs):
            _, A1, _, A2 = self.forward(X)
            loss = float(-np.mean(y * np.log(A2 + 1e-9) + (1 - y) * np.log(1 - A2 + 1e-9)))
            self.losses.append(loss)
            self.backward(X, y, A1, A2)

    def predict(self, X: np.ndarray) -> np.ndarray:
        _, _, _, A2 = self.forward(X)
        return (A2 >= 0.5).astype(int)

def mode_xor() -> None:
    print(f"[{nexdo_time()}] XOR 神经网络训练")
    # XOR 数据集
    X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
    y = np.array([[0], [1], [1], [0]], dtype=float)

    nn = NeuralNetwork(input_dim=2, hidden_dim=4, output_dim=1, lr=1.0)
    nn.train(X, y, epochs=5000)

    # 预测结果
    preds = nn.predict(X)
    rows = []
    for xi, yi, pi in zip(X, y, preds):
        correct = "✓" if int(yi[0]) == int(pi[0]) else "✗"
        rows.append([f"[{int(xi[0])},{int(xi[1])}]", int(yi[0]), int(pi[0]), correct])
    print_table(["输入", "真实", "预测", "正确"], rows, "XOR 预测结果")

    # 损失曲线(ASCII)
    sampled = nn.losses[::500]
    max_loss = max(sampled)
    print("\n  训练损失曲线(每500轮)")
    height = 8
    for h in range(height, 0, -1):
        threshold = h * max_loss / height
        line = f"  {threshold:6.3f} │"
        for l in sampled:
            line += "█" if l >= threshold else " "
        print(line)
    print(f"         └{'─'*len(sampled)}")
    print(f"  最终损失: {nn.losses[-1]:.6f}  准确率: {(preds == y.astype(int)).mean():.0%}")

mode_xor()

Step 3:用激活函数给神经网络装上转弯能力

痛点与机制

如果没有激活函数,多层神经网络再深也只是几次线性变换叠加,效果像只会走直线的车。Sigmoid、Tanh、ReLU 就像不同的方向盘:Sigmoid 适合把结果压成概率,ReLU 让正数直接通过、负数归零,训练时更利落。运行输出里的表格和 ASCII 曲线能帮读者把公式变成直觉。

核心源码(逐字来自文末完整源码)

def mode_activations() -> None:
    print(f"[{nexdo_time()}] 激活函数对比")
    x_vals = [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]

    def sigmoid(x: float) -> float:
        return 1 / (1 + math.exp(-x))

    def tanh(x: float) -> float:
        return math.tanh(x)

    def relu(x: float) -> float:
        return max(0.0, x)

    def leaky_relu(x: float) -> float:
        return x if x > 0 else 0.01 * x

    rows = []
    for x in x_vals:
        rows.append([f"{x:5.1f}",
                     f"{sigmoid(x):.4f}",
                     f"{tanh(x):.4f}",
                     f"{relu(x):.4f}",
                     f"{leaky_relu(x):.4f}"])
    print_table(["x", "Sigmoid", "Tanh", "ReLU", "LeakyReLU"], rows, "激活函数值对比")

    # ASCII 折线图(ReLU vs Sigmoid)
    print("\n  激活函数曲线(Sigmoid=S, ReLU=R)")
    width, height = 50, 10
    print(f"  {'─'*width}")
    for h in range(height, -1, -1):
        yv = h / height
        line = "  "
        for col in range(width):
            xv = -3.0 + col * 6.0 / width
            s_val = sigmoid(xv)
            r_val = min(1.0, max(0.0, relu(xv)))
            s_match = abs(s_val - yv) < 0.06
            r_match = abs(r_val - yv) < 0.06
            if s_match and r_match:
                line += "+"
            elif s_match:
                line += "S"
            elif r_match:
                line += "R"
            else:
                line += "·"
        print(line)
    print(f"  {'─'*width}")
    print(f"  x轴: [-3, +3]   y轴: [0, 1]   S=Sigmoid  R=ReLU")

可运行演示(补齐 Mock 数据与 print 反馈)

import math
import time
from typing import List

def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")

def print_table(headers: list, rows: list, title: str = "") -> None:
    if title:
        print(f"\n{'='*65}\n  {title}\n{'='*65}")
    col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
                  for i, h in enumerate(headers)]
    print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
    print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
    print(f"├{'┼'.join('─'*(w+2) for w in col_widths)}┤")
    for row in rows:
        print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
    print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")

def mode_activations() -> None:
    print(f"[{nexdo_time()}] 激活函数对比")
    x_vals = [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]

    def sigmoid(x: float) -> float:
        return 1 / (1 + math.exp(-x))

    def tanh(x: float) -> float:
        return math.tanh(x)

    def relu(x: float) -> float:
        return max(0.0, x)

    def leaky_relu(x: float) -> float:
        return x if x > 0 else 0.01 * x

    rows = []
    for x in x_vals:
        rows.append([f"{x:5.1f}",
                     f"{sigmoid(x):.4f}",
                     f"{tanh(x):.4f}",
                     f"{relu(x):.4f}",
                     f"{leaky_relu(x):.4f}"])
    print_table(["x", "Sigmoid", "Tanh", "ReLU", "LeakyReLU"], rows, "激活函数值对比")

    # ASCII 折线图(ReLU vs Sigmoid)
    print("\n  激活函数曲线(Sigmoid=S, ReLU=R)")
    width, height = 50, 10
    print(f"  {'─'*width}")
    for h in range(height, -1, -1):
        yv = h / height
        line = "  "
        for col in range(width):
            xv = -3.0 + col * 6.0 / width
            s_val = sigmoid(xv)
            r_val = min(1.0, max(0.0, relu(xv)))
            s_match = abs(s_val - yv) < 0.06
            r_match = abs(r_val - yv) < 0.06
            if s_match and r_match:
                line += "+"
            elif s_match:
                line += "S"
            elif r_match:
                line += "R"
            else:
                line += "·"
        print(line)
    print(f"  {'─'*width}")
    print(f"  x轴: [-3, +3]   y轴: [0, 1]   S=Sigmoid  R=ReLU")

mode_activations()

Step 4:用 OllamaClient 封装本地大模型接口

痛点与机制

本地大模型服务可以理解成一台开在 localhost 上的“私人大脑”。OllamaClient 的职责不是做智能推理,而是把 Python 字典打包成 JSON,通过 /api/chat/api/generate 这些接口送给 Ollama。这个演示只打印接口地址,不真实访问网络,保证没安装 Ollama 的读者也能先理解客户端封装。

核心源码(逐字来自文末完整源码)

class OllamaClient:
    """轻量 Ollama 客户端,仅用 urllib(零外部依赖)"""

    def __init__(self, base_url: str = "http://localhost:11434"):
        self.base_url = base_url

    def _post(self, path: str, payload: Dict[str, Any], timeout: int = 30) -> Dict:
        data = json.dumps(payload).encode()
        req = urllib.request.Request(
            f"{self.base_url}{path}",
            data=data,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read().decode())
        except urllib.error.URLError as e:
            return {"error": str(e)}

    def generate(self, model: str, prompt: str) -> str:
        result = self._post("/api/generate", {"model": model, "prompt": prompt, "stream": False})
        return result.get("response", result.get("error", "无响应"))

    def chat(self, model: str, messages: List[Dict[str, str]]) -> str:
        result = self._post("/api/chat", {"model": model, "messages": messages, "stream": False})
        return result.get("message", {}).get("content", result.get("error", "无响应"))

    def list_models(self) -> List[str]:
        try:
            req = urllib.request.Request(f"{self.base_url}/api/tags")
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode())
                return [m["name"] for m in data.get("models", [])]
        except Exception as e:
            return [f"连接失败: {e}"]

可运行演示(补齐 Mock 数据与 print 反馈)

import json
import urllib.error
import urllib.request
from typing import Any, Dict, List

class OllamaClient:
    """轻量 Ollama 客户端,仅用 urllib(零外部依赖)"""

    def __init__(self, base_url: str = "http://localhost:11434"):
        self.base_url = base_url

    def _post(self, path: str, payload: Dict[str, Any], timeout: int = 30) -> Dict:
        data = json.dumps(payload).encode()
        req = urllib.request.Request(
            f"{self.base_url}{path}",
            data=data,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read().decode())
        except urllib.error.URLError as e:
            return {"error": str(e)}

    def generate(self, model: str, prompt: str) -> str:
        result = self._post("/api/generate", {"model": model, "prompt": prompt, "stream": False})
        return result.get("response", result.get("error", "无响应"))

    def chat(self, model: str, messages: List[Dict[str, str]]) -> str:
        result = self._post("/api/chat", {"model": model, "messages": messages, "stream": False})
        return result.get("message", {}).get("content", result.get("error", "无响应"))

    def list_models(self) -> List[str]:
        try:
            req = urllib.request.Request(f"{self.base_url}/api/tags")
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode())
                return [m["name"] for m in data.get("models", [])]
        except Exception as e:
            return [f"连接失败: {e}"]

client = OllamaClient(base_url="http://localhost:11434")
print("Ollama 基础地址:", client.base_url)
print("模型列表接口:", f"{client.base_url}/api/tags")
print("聊天接口:", f"{client.base_url}/api/chat")
print("说明:这里先只看封装,不真实访问网络,避免本地没启动 Ollama 时报错。")

Step 5:用 LocalAIAgent 把用户问题路由到合适工具

痛点与机制

Agent 不神秘,可以先把它理解成前台分诊护士:用户说“计算”,就分到 math;说“写 Python”,就分到 code;说“分析数据”,就分到 analyze。_detect_intent() 先用关键词做轻量分流,run() 再组装系统提示词和历史消息。演示里用 Mock 客户端替代真实模型,是为了让读者立刻看见路由结果。

核心源码(逐字来自文末完整源码)

class LocalAIAgent:
    """本地 AI Agent 调度器"""

    TOOLS = {
        "math":    "数学计算与公式推导",
        "code":    "代码生成与调试",
        "explain": "概念解释与教学",
        "analyze": "数据分析与洞察",
    }

    def __init__(self, model: str = "llama3.2", ollama_url: str = "http://localhost:11434"):
        self.model = model
        self.client = OllamaClient(ollama_url)
        self.history: List[Dict[str, str]] = []

    def _detect_intent(self, query: str) -> str:
        """简单意图识别(关键词匹配,无需LLM)"""
        q = query.lower()
        if any(w in q for w in ["计算", "公式", "数学", "求", "等于"]):
            return "math"
        if any(w in q for w in ["代码", "python", "函数", "实现", "写"]):
            return "code"
        if any(w in q for w in ["分析", "数据", "统计", "趋势"]):
            return "analyze"
        return "explain"

    def run(self, query: str) -> Dict[str, Any]:
        intent = self._detect_intent(query)
        tool = self.TOOLS[intent]
        system_prompt = f"你是一个专注于{tool}的AI助手,请简洁准确地回答。"
        self.history.append({"role": "user", "content": query})
        messages = [{"role": "system", "content": system_prompt}] + self.history[-4:]
        response = self.client.chat(self.model, messages)
        self.history.append({"role": "assistant", "content": response})
        return {"intent": intent, "tool": tool, "response": response}

可运行演示(补齐 Mock 数据与 print 反馈)

from typing import Any, Dict, List

class OllamaClient:
    """演示用假客户端:不访问网络,只返回固定回答。"""
    def __init__(self, base_url: str = "mock://ollama"):
        self.base_url = base_url

    def chat(self, model: str, messages: List[Dict[str, str]]) -> str:
        last_user = messages[-1]["content"]
        return f"Mock模型收到:{last_user}"

class LocalAIAgent:
    """本地 AI Agent 调度器"""

    TOOLS = {
        "math":    "数学计算与公式推导",
        "code":    "代码生成与调试",
        "explain": "概念解释与教学",
        "analyze": "数据分析与洞察",
    }

    def __init__(self, model: str = "llama3.2", ollama_url: str = "http://localhost:11434"):
        self.model = model
        self.client = OllamaClient(ollama_url)
        self.history: List[Dict[str, str]] = []

    def _detect_intent(self, query: str) -> str:
        """简单意图识别(关键词匹配,无需LLM)"""
        q = query.lower()
        if any(w in q for w in ["计算", "公式", "数学", "求", "等于"]):
            return "math"
        if any(w in q for w in ["代码", "python", "函数", "实现", "写"]):
            return "code"
        if any(w in q for w in ["分析", "数据", "统计", "趋势"]):
            return "analyze"
        return "explain"

    def run(self, query: str) -> Dict[str, Any]:
        intent = self._detect_intent(query)
        tool = self.TOOLS[intent]
        system_prompt = f"你是一个专注于{tool}的AI助手,请简洁准确地回答。"
        self.history.append({"role": "user", "content": query})
        messages = [{"role": "system", "content": system_prompt}] + self.history[-4:]
        response = self.client.chat(self.model, messages)
        self.history.append({"role": "assistant", "content": response})
        return {"intent": intent, "tool": tool, "response": response}

agent = LocalAIAgent(model="mock-llm", ollama_url="mock://ollama")
queries = [
    "帮我计算 8 乘以 7",
    "用 Python 写一个求和函数",
    "分析这组销售数据趋势",
    "解释什么是反向传播",
]

for query in queries:
    result = agent.run(query)
    print(f"问题: {query}")
    print(f"意图: {result['intent']} | 工具: {result['tool']}")
    print(f"回答: {result['response']}")
    print("-" * 50)

Step 6:用 mode_agent 演示 Ollama 缺席时的优雅降级

痛点与机制

工程代码不能假设外部服务永远在线。mode_agent() 先检查模型列表,如果发现 Ollama 没启动,就跳过真实调用,但仍展示意图识别和架构流程。这像餐厅发现后厨暂时停火:不能继续上菜,但前台仍要清楚告诉客人原因和下一步怎么做。

核心源码(逐字来自文末完整源码)

def mode_agent() -> None:
    print(f"[{nexdo_time()}] 本地 AI Agent 调度器演示")

    # 检查 Ollama 是否可用
    client = OllamaClient()
    models = client.list_models()
    ollama_available = not any("连接失败" in m for m in models)

    rows = [
        ["Ollama服务", "http://localhost:11434", "✓ 在线" if ollama_available else "✗ 未启动"],
        ["可用模型", ", ".join(models[:3]) if ollama_available else "N/A", ""],
    ]
    print_table(["组件", "地址/值", "状态"], rows, "AI Agent 环境检查")

    # 演示意图识别(不依赖Ollama)
    agent = LocalAIAgent()
    test_queries = [
        "帮我计算 1+1 等于多少",
        "用Python写一个快速排序",
        "解释什么是梯度下降",
        "分析这组数据的趋势",
    ]
    intent_rows = []
    for q in test_queries:
        intent = agent._detect_intent(q)
        intent_rows.append([q[:25]+"...", intent, LocalAIAgent.TOOLS[intent]])
    print_table(["查询", "识别意图", "调用工具"], intent_rows, "意图识别演示(无需Ollama)")

    if ollama_available:
        print(f"\n[{nexdo_time()}] 调用 Ollama 生成回答...")
        result = agent.run("用一句话解释什么是反向传播")
        print(f"\n  意图: {result['intent']}  工具: {result['tool']}")
        print(f"  回答: {result['response'][:200]}")
    else:
        print(f"\n  ℹ Ollama 未启动,跳过实际调用。")
        print(f"  启动方式: ollama serve  然后: ollama pull llama3.2")

    # 展示 Agent 调度架构
    print("""
  Agent 调度流程:
  ┌──────────────────────────────────────────────────────┐
  │  用户输入 → 意图识别 → 工具路由 → LLM调用 → 返回    │
  │                                                      │
  │  意图识别:关键词匹配(轻量,无需LLM)               │
  │  工具路由:math/code/explain/analyze                 │
  │  LLM调用:POST /api/chat → Ollama本地推理            │
  │  历史管理:保留最近4轮对话上下文                     │
  └──────────────────────────────────────────────────────┘
    """)

可运行演示(补齐 Mock 数据与 print 反馈)

import time
from typing import Any, Dict, List

def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")

def print_table(headers: list, rows: list, title: str = "") -> None:
    if title:
        print(f"\n{'='*65}\n  {title}\n{'='*65}")
    col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
                  for i, h in enumerate(headers)]
    print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
    print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
    print(f"├{'┼'.join('─'*(w+2) for w in col_widths)}┤")
    for row in rows:
        print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
    print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")

class OllamaClient:
    """演示用假客户端:模拟 Ollama 未启动,但程序仍能优雅降级。"""
    def __init__(self, base_url: str = "http://localhost:11434"):
        self.base_url = base_url

    def list_models(self) -> List[str]:
        return ["连接失败: 演示环境未启动 Ollama"]

    def chat(self, model: str, messages: List[Dict[str, str]]) -> str:
        return "演示环境跳过真实大模型调用"

class LocalAIAgent:
    """本地 AI Agent 调度器"""

    TOOLS = {
        "math":    "数学计算与公式推导",
        "code":    "代码生成与调试",
        "explain": "概念解释与教学",
        "analyze": "数据分析与洞察",
    }

    def __init__(self, model: str = "llama3.2", ollama_url: str = "http://localhost:11434"):
        self.model = model
        self.client = OllamaClient(ollama_url)
        self.history: List[Dict[str, str]] = []

    def _detect_intent(self, query: str) -> str:
        """简单意图识别(关键词匹配,无需LLM)"""
        q = query.lower()
        if any(w in q for w in ["计算", "公式", "数学", "求", "等于"]):
            return "math"
        if any(w in q for w in ["代码", "python", "函数", "实现", "写"]):
            return "code"
        if any(w in q for w in ["分析", "数据", "统计", "趋势"]):
            return "analyze"
        return "explain"

    def run(self, query: str) -> Dict[str, Any]:
        intent = self._detect_intent(query)
        tool = self.TOOLS[intent]
        system_prompt = f"你是一个专注于{tool}的AI助手,请简洁准确地回答。"
        self.history.append({"role": "user", "content": query})
        messages = [{"role": "system", "content": system_prompt}] + self.history[-4:]
        response = self.client.chat(self.model, messages)
        self.history.append({"role": "assistant", "content": response})
        return {"intent": intent, "tool": tool, "response": response}

def mode_agent() -> None:
    print(f"[{nexdo_time()}] 本地 AI Agent 调度器演示")

    # 检查 Ollama 是否可用
    client = OllamaClient()
    models = client.list_models()
    ollama_available = not any("连接失败" in m for m in models)

    rows = [
        ["Ollama服务", "http://localhost:11434", "✓ 在线" if ollama_available else "✗ 未启动"],
        ["可用模型", ", ".join(models[:3]) if ollama_available else "N/A", ""],
    ]
    print_table(["组件", "地址/值", "状态"], rows, "AI Agent 环境检查")

    # 演示意图识别(不依赖Ollama)
    agent = LocalAIAgent()
    test_queries = [
        "帮我计算 1+1 等于多少",
        "用Python写一个快速排序",
        "解释什么是梯度下降",
        "分析这组数据的趋势",
    ]
    intent_rows = []
    for q in test_queries:
        intent = agent._detect_intent(q)
        intent_rows.append([q[:25]+"...", intent, LocalAIAgent.TOOLS[intent]])
    print_table(["查询", "识别意图", "调用工具"], intent_rows, "意图识别演示(无需Ollama)")

    if ollama_available:
        print(f"\n[{nexdo_time()}] 调用 Ollama 生成回答...")
        result = agent.run("用一句话解释什么是反向传播")
        print(f"\n  意图: {result['intent']}  工具: {result['tool']}")
        print(f"  回答: {result['response'][:200]}")
    else:
        print(f"\n  ℹ Ollama 未启动,跳过实际调用。")
        print(f"  启动方式: ollama serve  然后: ollama pull llama3.2")

    # 展示 Agent 调度架构
    print("""
  Agent 调度流程:
  ┌──────────────────────────────────────────────────────┐
  │  用户输入 → 意图识别 → 工具路由 → LLM调用 → 返回    │
  │                                                      │
  │  意图识别:关键词匹配(轻量,无需LLM)               │
  │  工具路由:math/code/explain/analyze                 │
  │  LLM调用:POST /api/chat → Ollama本地推理            │
  │  历史管理:保留最近4轮对话上下文                     │
  └──────────────────────────────────────────────────────┘
    """)

mode_agent()

Step 7:用 main 把多个演示模块做成命令行工具

痛点与机制

教程脚本最终要交给普通用户运行,argparse 就是遥控器。用户不需要改源码,只要输入 --mode xor--mode activations--mode agent,就能切换不同演示。这个设计比临时在终端里等待键盘输入更稳定,也更适合复制到终端、CI 或服务器里执行。

核心源码(逐字来自文末完整源码)

def main() -> None:
    parser = argparse.ArgumentParser(description="神经网络原理与本地AI Agent演示")
    parser.add_argument("--mode", choices=["xor", "activations", "agent", "all"],
                        default="all")
    args = parser.parse_args()
    dispatch = {
        "xor":         mode_xor,
        "activations": mode_activations,
        "agent":       mode_agent,
        "all":         lambda: [mode_xor(), mode_activations(), mode_agent()],
    }
    dispatch[args.mode]()
    print(f"\n[{nexdo_time()}] 完成")

可运行演示(补齐 Mock 数据与 print 反馈)

import argparse
import sys
import time

def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")

def mode_xor() -> None:
    print("执行 XOR 神经网络训练模块")


def mode_activations() -> None:
    print("执行激活函数对比模块")


def mode_agent() -> None:
    print("执行本地 AI Agent 调度模块")

def main() -> None:
    parser = argparse.ArgumentParser(description="神经网络原理与本地AI Agent演示")
    parser.add_argument("--mode", choices=["xor", "activations", "agent", "all"],
                        default="all")
    args = parser.parse_args()
    dispatch = {
        "xor":         mode_xor,
        "activations": mode_activations,
        "agent":       mode_agent,
        "all":         lambda: [mode_xor(), mode_activations(), mode_agent()],
    }
    dispatch[args.mode]()
    print(f"\n[{nexdo_time()}] 完成")

for mode in ["xor", "activations", "agent"]:
    print(f"\n$ python 47-python-neural-agent.py --mode {mode}")
    sys.argv = ["47-python-neural-agent.py", "--mode", mode]
    main()

极客实战:完整源码与运行

现在,把上面的积木拼起来,将下面完整代码保存为 47-python-neural-agent.py。神经网络部分完全本地运行;Agent 部分会优雅检查 Ollama 是否启动,未启动时也会给出清晰提示,不会让脚本崩掉。

#!/usr/bin/env python3
"""
47-neural-agent.py
① numpy从零实现3层神经网络,训练XOR问题
② 本地AI Agent调度器(urllib调用Ollama,零外部依赖)

用法:
  python 47-neural-agent.py --mode xor        # 训练XOR神经网络
  python 47-neural-agent.py --mode activations # 激活函数对比
  python 47-neural-agent.py --mode agent       # AI Agent调度器演示
  python 47-neural-agent.py --mode all
"""

import argparse
import json
import math
import time
import urllib.request
import urllib.error
from typing import List, Tuple, Dict, Any

import numpy as np


def nexdo_time() -> str:
    return time.strftime("%Y-%m-%d %H:%M:%S")


def print_table(headers: list, rows: list, title: str = "") -> None:
    if title:
        print(f"\n{'='*65}\n  {title}\n{'='*65}")
    col_widths = [max(len(str(h)), max((len(str(r[i])) for r in rows), default=0))
                  for i, h in enumerate(headers)]
    print(f"┌{'┬'.join('─'*(w+2) for w in col_widths)}┐")
    print(f"│{'│'.join(f' {str(h):<{w}} ' for h, w in zip(headers, col_widths))}│")
    print(f"├{'┼'.join('─'*(w+2) for w in col_widths)}┤")
    for row in rows:
        print(f"│{'│'.join(f' {str(v):<{w}} ' for v, w in zip(row, col_widths))}│")
    print(f"└{'┴'.join('─'*(w+2) for w in col_widths)}┘")


# ── 3层神经网络(numpy实现)─────────────────────────────────────────────────





class NeuralNetwork:
    """3层全连接神经网络:输入层→隐藏层→输出层"""

    def __init__(self, input_dim: int, hidden_dim: int, output_dim: int,
                 lr: float = 0.1):
        rng = np.random.RandomState(42)
        self.W1 = rng.randn(input_dim, hidden_dim) * 0.5
        self.b1 = np.zeros((1, hidden_dim))
        self.W2 = rng.randn(hidden_dim, output_dim) * 0.5
        self.b2 = np.zeros((1, output_dim))
        self.lr = lr
        self.losses: List[float] = []

    @staticmethod
    def sigmoid(z: np.ndarray) -> np.ndarray:
        return 1.0 / (1.0 + np.exp(-np.clip(z, -500, 500)))

    @staticmethod
    def sigmoid_deriv(a: np.ndarray) -> np.ndarray:
        return a * (1.0 - a)

    def forward(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        Z1 = X @ self.W1 + self.b1
        A1 = self.sigmoid(Z1)
        Z2 = A1 @ self.W2 + self.b2
        A2 = self.sigmoid(Z2)
        return Z1, A1, Z2, A2

    def backward(self, X: np.ndarray, y: np.ndarray,
                 A1: np.ndarray, A2: np.ndarray) -> None:
        n = X.shape[0]
        delta2 = (A2 - y) * self.sigmoid_deriv(A2)
        dW2 = A1.T @ delta2 / n
        db2 = delta2.mean(axis=0, keepdims=True)
        delta1 = (delta2 @ self.W2.T) * self.sigmoid_deriv(A1)
        dW1 = X.T @ delta1 / n
        db1 = delta1.mean(axis=0, keepdims=True)
        self.W1 -= self.lr * dW1
        self.b1 -= self.lr * db1
        self.W2 -= self.lr * dW2
        self.b2 -= self.lr * db2

    def train(self, X: np.ndarray, y: np.ndarray, epochs: int = 10000) -> None:
        for _ in range(epochs):
            _, A1, _, A2 = self.forward(X)
            loss = float(-np.mean(y * np.log(A2 + 1e-9) + (1 - y) * np.log(1 - A2 + 1e-9)))
            self.losses.append(loss)
            self.backward(X, y, A1, A2)

    def predict(self, X: np.ndarray) -> np.ndarray:
        _, _, _, A2 = self.forward(X)
        return (A2 >= 0.5).astype(int)


def mode_xor() -> None:
    print(f"[{nexdo_time()}] XOR 神经网络训练")
    # XOR 数据集
    X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
    y = np.array([[0], [1], [1], [0]], dtype=float)

    nn = NeuralNetwork(input_dim=2, hidden_dim=4, output_dim=1, lr=1.0)
    nn.train(X, y, epochs=5000)

    # 预测结果
    preds = nn.predict(X)
    rows = []
    for xi, yi, pi in zip(X, y, preds):
        correct = "✓" if int(yi[0]) == int(pi[0]) else "✗"
        rows.append([f"[{int(xi[0])},{int(xi[1])}]", int(yi[0]), int(pi[0]), correct])
    print_table(["输入", "真实", "预测", "正确"], rows, "XOR 预测结果")

    # 损失曲线(ASCII)
    sampled = nn.losses[::500]
    max_loss = max(sampled)
    print("\n  训练损失曲线(每500轮)")
    height = 8
    for h in range(height, 0, -1):
        threshold = h * max_loss / height
        line = f"  {threshold:6.3f} │"
        for l in sampled:
            line += "█" if l >= threshold else " "
        print(line)
    print(f"         └{'─'*len(sampled)}")
    print(f"  最终损失: {nn.losses[-1]:.6f}  准确率: {(preds == y.astype(int)).mean():.0%}")


def mode_activations() -> None:
    print(f"[{nexdo_time()}] 激活函数对比")
    x_vals = [-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0]

    def sigmoid(x: float) -> float:
        return 1 / (1 + math.exp(-x))

    def tanh(x: float) -> float:
        return math.tanh(x)

    def relu(x: float) -> float:
        return max(0.0, x)

    def leaky_relu(x: float) -> float:
        return x if x > 0 else 0.01 * x

    rows = []
    for x in x_vals:
        rows.append([f"{x:5.1f}",
                     f"{sigmoid(x):.4f}",
                     f"{tanh(x):.4f}",
                     f"{relu(x):.4f}",
                     f"{leaky_relu(x):.4f}"])
    print_table(["x", "Sigmoid", "Tanh", "ReLU", "LeakyReLU"], rows, "激活函数值对比")

    # ASCII 折线图(ReLU vs Sigmoid)
    print("\n  激活函数曲线(Sigmoid=S, ReLU=R)")
    width, height = 50, 10
    print(f"  {'─'*width}")
    for h in range(height, -1, -1):
        yv = h / height
        line = "  "
        for col in range(width):
            xv = -3.0 + col * 6.0 / width
            s_val = sigmoid(xv)
            r_val = min(1.0, max(0.0, relu(xv)))
            s_match = abs(s_val - yv) < 0.06
            r_match = abs(r_val - yv) < 0.06
            if s_match and r_match:
                line += "+"
            elif s_match:
                line += "S"
            elif r_match:
                line += "R"
            else:
                line += "·"
        print(line)
    print(f"  {'─'*width}")
    print(f"  x轴: [-3, +3]   y轴: [0, 1]   S=Sigmoid  R=ReLU")


# ── 本地 AI Agent 调度器 ─────────────────────────────────────────────────────

class OllamaClient:
    """轻量 Ollama 客户端,仅用 urllib(零外部依赖)"""

    def __init__(self, base_url: str = "http://localhost:11434"):
        self.base_url = base_url

    def _post(self, path: str, payload: Dict[str, Any], timeout: int = 30) -> Dict:
        data = json.dumps(payload).encode()
        req = urllib.request.Request(
            f"{self.base_url}{path}",
            data=data,
            headers={"Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urllib.request.urlopen(req, timeout=timeout) as resp:
                return json.loads(resp.read().decode())
        except urllib.error.URLError as e:
            return {"error": str(e)}

    def generate(self, model: str, prompt: str) -> str:
        result = self._post("/api/generate", {"model": model, "prompt": prompt, "stream": False})
        return result.get("response", result.get("error", "无响应"))

    def chat(self, model: str, messages: List[Dict[str, str]]) -> str:
        result = self._post("/api/chat", {"model": model, "messages": messages, "stream": False})
        return result.get("message", {}).get("content", result.get("error", "无响应"))

    def list_models(self) -> List[str]:
        try:
            req = urllib.request.Request(f"{self.base_url}/api/tags")
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode())
                return [m["name"] for m in data.get("models", [])]
        except Exception as e:
            return [f"连接失败: {e}"]


class LocalAIAgent:
    """本地 AI Agent 调度器"""

    TOOLS = {
        "math":    "数学计算与公式推导",
        "code":    "代码生成与调试",
        "explain": "概念解释与教学",
        "analyze": "数据分析与洞察",
    }

    def __init__(self, model: str = "llama3.2", ollama_url: str = "http://localhost:11434"):
        self.model = model
        self.client = OllamaClient(ollama_url)
        self.history: List[Dict[str, str]] = []

    def _detect_intent(self, query: str) -> str:
        """简单意图识别(关键词匹配,无需LLM)"""
        q = query.lower()
        if any(w in q for w in ["计算", "公式", "数学", "求", "等于"]):
            return "math"
        if any(w in q for w in ["代码", "python", "函数", "实现", "写"]):
            return "code"
        if any(w in q for w in ["分析", "数据", "统计", "趋势"]):
            return "analyze"
        return "explain"

    def run(self, query: str) -> Dict[str, Any]:
        intent = self._detect_intent(query)
        tool = self.TOOLS[intent]
        system_prompt = f"你是一个专注于{tool}的AI助手,请简洁准确地回答。"
        self.history.append({"role": "user", "content": query})
        messages = [{"role": "system", "content": system_prompt}] + self.history[-4:]
        response = self.client.chat(self.model, messages)
        self.history.append({"role": "assistant", "content": response})
        return {"intent": intent, "tool": tool, "response": response}


def mode_agent() -> None:
    print(f"[{nexdo_time()}] 本地 AI Agent 调度器演示")

    # 检查 Ollama 是否可用
    client = OllamaClient()
    models = client.list_models()
    ollama_available = not any("连接失败" in m for m in models)

    rows = [
        ["Ollama服务", "http://localhost:11434", "✓ 在线" if ollama_available else "✗ 未启动"],
        ["可用模型", ", ".join(models[:3]) if ollama_available else "N/A", ""],
    ]
    print_table(["组件", "地址/值", "状态"], rows, "AI Agent 环境检查")

    # 演示意图识别(不依赖Ollama)
    agent = LocalAIAgent()
    test_queries = [
        "帮我计算 1+1 等于多少",
        "用Python写一个快速排序",
        "解释什么是梯度下降",
        "分析这组数据的趋势",
    ]
    intent_rows = []
    for q in test_queries:
        intent = agent._detect_intent(q)
        intent_rows.append([q[:25]+"...", intent, LocalAIAgent.TOOLS[intent]])
    print_table(["查询", "识别意图", "调用工具"], intent_rows, "意图识别演示(无需Ollama)")

    if ollama_available:
        print(f"\n[{nexdo_time()}] 调用 Ollama 生成回答...")
        result = agent.run("用一句话解释什么是反向传播")
        print(f"\n  意图: {result['intent']}  工具: {result['tool']}")
        print(f"  回答: {result['response'][:200]}")
    else:
        print(f"\n  ℹ Ollama 未启动,跳过实际调用。")
        print(f"  启动方式: ollama serve  然后: ollama pull llama3.2")

    # 展示 Agent 调度架构
    print("""
  Agent 调度流程:
  ┌──────────────────────────────────────────────────────┐
  │  用户输入 → 意图识别 → 工具路由 → LLM调用 → 返回    │
  │                                                      │
  │  意图识别:关键词匹配(轻量,无需LLM)               │
  │  工具路由:math/code/explain/analyze                 │
  │  LLM调用:POST /api/chat → Ollama本地推理            │
  │  历史管理:保留最近4轮对话上下文                     │
  └──────────────────────────────────────────────────────┘
    """)


def main() -> None:
    parser = argparse.ArgumentParser(description="神经网络原理与本地AI Agent演示")
    parser.add_argument("--mode", choices=["xor", "activations", "agent", "all"],
                        default="all")
    args = parser.parse_args()
    dispatch = {
        "xor":         mode_xor,
        "activations": mode_activations,
        "agent":       mode_agent,
        "all":         lambda: [mode_xor(), mode_activations(), mode_agent()],
    }
    dispatch[args.mode]()
    print(f"\n[{nexdo_time()}] 完成")


if __name__ == "__main__":
    main()
$ python 47-python-neural-agent.py --mode xor
[2026-04-18 11:34:14] XOR 神经网络训练

=================================================================
  XOR 预测结果
=================================================================
┌───────┬────┬────┬────┐
│ 输入    │ 真实 │ 预测 │ 正确 │
├───────┼────┼────┼────┤
[0,0]00  │ ✓  │
[0,1]11  │ ✓  │
[1,0]11  │ ✓  │
[1,1]00  │ ✓  │
└───────┴────┴────┴────┘

  训练损失曲线(每500轮)
   0.705 │█         
   0.616 │████      
   0.528 │█████     
   0.440 │█████     
   0.352 │██████    
   0.264 │██████    
   0.176 │██████    
   0.088 │████████  
         └──────────
  最终损失: 0.052908  准确率: 100%

[2026-04-18 11:34:14] 完成

$ python 47-python-neural-agent.py --mode activations
[2026-04-18 11:34:15] 激活函数对比

=================================================================
  激活函数值对比
=================================================================
┌───────┬─────────┬─────────┬────────┬───────────┐
│ x     │ Sigmoid │ Tanh    │ ReLU   │ LeakyReLU │
├───────┼─────────┼─────────┼────────┼───────────┤
│  -3.0 │ 0.0474  │ -0.9951 │ 0.0000 │ -0.0300   │
│  -2.0 │ 0.1192  │ -0.9640 │ 0.0000 │ -0.0200   │
│  -1.0 │ 0.2689  │ -0.7616 │ 0.0000 │ -0.0100   │
│   0.0 │ 0.5000  │ 0.0000  │ 0.0000 │ 0.0000    │
│   1.0 │ 0.7311  │ 0.7616  │ 1.0000 │ 1.0000    │
│   2.0 │ 0.8808  │ 0.9640  │ 2.0000 │ 2.0000    │
│   3.0 │ 0.9526  │ 0.9951  │ 3.0000 │ 3.0000    │
└───────┴─────────┴─────────┴────────┴───────────┘

  激活函数曲线(Sigmoid=S, ReLU=R)
  ──────────────────────────────────────────────────
  ·································RRRRRRRRRRRRRRR++
  ·································R·····SSSSSSSSSSS
  ································R·SSSSSSS·········
  ······························S+SSS···············
  ···························SSS+···················
  ·······················SSSSS·R····················
  ····················SSSS····R·····················
  ················SSSSS······RR·····················
  ··········SSSSSSS··········R······················
  SSSSSSSSSSSS··············R·······················
  +++RRRRRRRRRRRRRRRRRRRRRRR························
  ──────────────────────────────────────────────────
  x轴: [-3, +3]   y轴: [0, 1]   S=Sigmoid  R=ReLU

小结与 NexDo Time ⚡

这一篇你完成了两件关键事:第一,用 NumPy 手写了一个能学习 XOR 的三层神经网络;第二,用 Ollama 接口思路搭了一个本地 Agent 调度骨架。前者训练的是“模型参数”,后者组织的是“任务流程”,两者合在一起,就是现代 AI 应用的底层肌肉。

5 分钟微操挑战:把 mode_xor() 里的隐藏层神经元从 4 改成 2816,分别观察最终损失和准确率变化。然后思考一个问题:隐藏层越大一定越好吗?

Don’t wait for next time, do it in the next moment.