Week 3 Day 19:RBAC 权限系统 - 苏格拉底教学

当你的 Agent 能操作多个系统、多种资源时,谁能做什么、谁不能做什么,就不再是细节,而是安全边界。今天我们用 RBAC 把权限控制从"散落检查"升级为"可维护的策略"。


第一部分:问题驱动

问题1:为什么不能在代码里直接写 if user == "admin"

引导问题:

  1. 你有一个 Agent,它能搜索知识库、创建工单、查用户信息。现在来了一个需求:普通员工只能搜索,只有 HR 能查用户信息,只有 L2 Support 能创建工单——你怎么实现?
  2. 如果你在每个 handler 里写 if user.Department == "HR",6个月后新来一个功能,你还记得要在哪里加检查吗?
  3. 如果权限规则变了(比如 HR 现在也能创建工单),你需要改几个地方?

答案揭示:

"散落检查"(Ad-hoc checks)的问题:

  • 遗漏:新 handler 容易忘记加检查
  • 不一致:相同资源的检查逻辑可能写法不同
  • 难审计:安全审计时,无法快速看到"谁能做什么"
  • 难修改:规则变更需要全局搜索修改

RBAC(Role-Based Access Control)的优势:

  • 权限集中声明,一处修改,处处生效
  • 新资源加入只需在权限矩阵里加一行
  • 审计时有清晰的 Role→Permission 映射

问题2:RBAC 的核心概念是什么?

引导问题:

  1. "角色"和"权限"是什么关系?是 1:1 还是 N:M?
  2. 一个用户可以有多个角色吗?
  3. "resource" 和 "action" 怎么组合成一个 permission?

三元模型:

User → [has] → Role(s) → [has] → Permission(s) Permission = action + resource 例如: - "read" + "knowledge_base" → 搜索知识库权限 - "create" + "ticket" → 创建工单权限 - "read" + "user_info" → 查询用户信息权限 - "approve" + "ticket" → 审批工单权限

Policy 的核心问题:can(user, action, resource) → bool


问题3:权限矩阵应该长什么样?

用表格思考:

Role search_kb create_ticket read_user_info approve_ticket manage_roles
employee
l1_support
l2_support
hr
admin

引导问题:

  • 如果 L2 Support 也需要查用户信息,只需改矩阵哪里?
  • admin 能不能用"继承"来代替把所有权限都列出来?(角色继承)

第二部分:动手实现

版本1:基础数据结构

// internal/rbac/types.go
package rbac

// Action 代表操作类型
type Action string

const (
    ActionRead    Action = "read"
    ActionCreate  Action = "create"
    ActionUpdate  Action = "update"
    ActionDelete  Action = "delete"
    ActionApprove Action = "approve"
    ActionManage  Action = "manage"
)

// Resource 代表被操作的资源
type Resource string

const (
    ResourceKnowledgeBase Resource = "knowledge_base"
    ResourceTicket        Resource = "ticket"
    ResourceUserInfo      Resource = "user_info"
    ResourceRole          Resource = "role"
    ResourceChunk         Resource = "chunk"
)

// Permission 是 action + resource 的组合
type Permission struct {
    Action   Action   `json:"action"`
    Resource Resource `json:"resource"`
}

func (p Permission) String() string {
    return string(p.Action) + ":" + string(p.Resource)
}

// NewPermission 创建权限对象
func NewPermission(action Action, resource Resource) Permission {
    return Permission{Action: action, Resource: resource}
}

// Role 是一组权限的集合
type Role struct {
    Name        string       `json:"name"`
    Description string       `json:"description"`
    Permissions []Permission `json:"permissions"`
    // 可选:继承父角色
    Inherits []string `json:"inherits,omitempty"`
}

// User 持有一个或多个角色
type User struct {
    ID    string   `json:"id"`
    Name  string   `json:"name"`
    Email string   `json:"email"`
    Roles []string `json:"roles"` // 角色名列表
}

版本2:权限注册表 + Policy Check

// internal/rbac/registry.go
package rbac

import (
    "fmt"
    "sync"
)

// Registry 维护所有 Role 和权限规则
type Registry struct {
    mu    sync.RWMutex
    roles map[string]*Role
}

func NewRegistry() *Registry {
    r := &Registry{
        roles: make(map[string]*Role),
    }
    // 注册默认角色
    r.registerDefaultRoles()
    return r
}

func (r *Registry) RegisterRole(role *Role) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.roles[role.Name] = role
}

func (r *Registry) GetRole(name string) (*Role, bool) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    role, ok := r.roles[name]
    return role, ok
}

// GetEffectivePermissions 获取角色的所有权限(包含继承)
func (r *Registry) GetEffectivePermissions(roleName string) []Permission {
    r.mu.RLock()
    defer r.mu.RUnlock()
    return r.collectPermissions(roleName, make(map[string]bool))
}

// collectPermissions 递归收集权限,visited 防止循环继承
func (r *Registry) collectPermissions(roleName string, visited map[string]bool) []Permission {
    if visited[roleName] {
        return nil
    }
    visited[roleName] = true

    role, ok := r.roles[roleName]
    if !ok {
        return nil
    }

    perms := make([]Permission, len(role.Permissions))
    copy(perms, role.Permissions)

    // 递归处理父角色
    for _, parent := range role.Inherits {
        parentPerms := r.collectPermissions(parent, visited)
        perms = append(perms, parentPerms...)
    }

    return perms
}

func (r *Registry) registerDefaultRoles() {
    roles := []*Role{
        {
            Name:        "employee",
            Description: "普通员工:只能搜索知识库",
            Permissions: []Permission{
                NewPermission(ActionRead, ResourceKnowledgeBase),
            },
        },
        {
            Name:        "l1_support",
            Description: "L1 支持:可以创建工单",
            Inherits:    []string{"employee"},
            Permissions: []Permission{
                NewPermission(ActionCreate, ResourceTicket),
                NewPermission(ActionRead, ResourceTicket),
            },
        },
        {
            Name:        "l2_support",
            Description: "L2 支持:可以审批工单",
            Inherits:    []string{"l1_support"},
            Permissions: []Permission{
                NewPermission(ActionApprove, ResourceTicket),
            },
        },
        {
            Name:        "hr",
            Description: "HR:可以查用户信息",
            Inherits:    []string{"employee"},
            Permissions: []Permission{
                NewPermission(ActionRead, ResourceUserInfo),
            },
        },
        {
            Name:        "admin",
            Description: "管理员:拥有所有权限",
            Inherits:    []string{"l2_support", "hr"},
            Permissions: []Permission{
                NewPermission(ActionManage, ResourceRole),
            },
        },
    }

    for _, role := range roles {
        r.roles[role.Name] = role
    }
}

版本3:Policy Checker(核心)

// internal/rbac/policy.go
package rbac

import (
    "context"
    "fmt"
    "log/slog"
)

// PolicyChecker 是 RBAC 的核心:can(user, action, resource)
type PolicyChecker struct {
    registry *Registry
    userRepo UserRepository
}

// UserRepository 抽象:获取用户角色信息
type UserRepository interface {
    GetUserRoles(ctx context.Context, userID string) ([]string, error)
}

func NewPolicyChecker(registry *Registry, repo UserRepository) *PolicyChecker {
    return &PolicyChecker{
        registry: registry,
        userRepo: repo,
    }
}

// Can 核心方法:用户能否对资源执行操作
func (p *PolicyChecker) Can(ctx context.Context, userID string, action Action, resource Resource) (bool, error) {
    // 1. 获取用户的角色
    roles, err := p.userRepo.GetUserRoles(ctx, userID)
    if err != nil {
        return false, fmt.Errorf("get user roles: %w", err)
    }

    // 2. 遍历所有角色的权限(含继承)
    required := NewPermission(action, resource)

    for _, roleName := range roles {
        perms := p.registry.GetEffectivePermissions(roleName)
        for _, perm := range perms {
            if perm == required {
                slog.Debug("permission granted",
                    "user_id", userID,
                    "action", action,
                    "resource", resource,
                    "role", roleName,
                )
                return true, nil
            }
        }
    }

    slog.Warn("permission denied",
        "user_id", userID,
        "action", action,
        "resource", resource,
        "roles", roles,
    )
    return false, nil
}

// MustCan 如果没权限直接返回 error(方便在 handler 里使用)
func (p *PolicyChecker) MustCan(ctx context.Context, userID string, action Action, resource Resource) error {
    ok, err := p.Can(ctx, userID, action, resource)
    if err != nil {
        return fmt.Errorf("checking permission: %w", err)
    }
    if !ok {
        return &PermissionDeniedError{
            UserID:   userID,
            Action:   action,
            Resource: resource,
        }
    }
    return nil
}

// PermissionDeniedError 专用错误类型,方便上层处理
type PermissionDeniedError struct {
    UserID   string
    Action   Action
    Resource Resource
}

func (e *PermissionDeniedError) Error() string {
    return fmt.Sprintf("user %s is not allowed to %s on %s",
        e.UserID, e.Action, e.Resource)
}

版本4:集成 Tool Registry(工具声明权限)

// internal/tools/registry.go
package tools

import (
    "context"
    "fmt"
    "your-project/internal/rbac"
)

// ToolDefinition 描述一个工具及其所需权限
type ToolDefinition struct {
    Name        string          `json:"name"`
    Description string          `json:"description"`
    // 每个工具需要的权限(调用前会检查)
    RequiredPermissions []rbac.Permission `json:"required_permissions"`
    // 实际执行函数
    Handler func(ctx context.Context, args map[string]interface{}) (interface{}, error)
}

// ToolRegistry 管理所有可用工具
type ToolRegistry struct {
    tools   map[string]*ToolDefinition
    checker *rbac.PolicyChecker
}

func NewToolRegistry(checker *rbac.PolicyChecker) *ToolRegistry {
    tr := &ToolRegistry{
        tools:   make(map[string]*ToolDefinition),
        checker: checker,
    }
    tr.registerDefaultTools()
    return tr
}

func (tr *ToolRegistry) RegisterTool(def *ToolDefinition) {
    tr.tools[def.Name] = def
}

// ExecuteTool 执行工具前先检查权限
func (tr *ToolRegistry) ExecuteTool(
    ctx context.Context,
    userID string,
    toolName string,
    args map[string]interface{},
) (interface{}, error) {
    tool, ok := tr.tools[toolName]
    if !ok {
        return nil, fmt.Errorf("tool %q not found", toolName)
    }

    // 检查所有所需权限
    for _, perm := range tool.RequiredPermissions {
        if err := tr.checker.MustCan(ctx, userID, perm.Action, perm.Resource); err != nil {
            return nil, fmt.Errorf("tool %q requires %s: %w", toolName, perm, err)
        }
    }

    // 权限通过,执行工具
    return tool.Handler(ctx, args)
}

// GetAvailableTools 返回用户有权限使用的工具列表(用于构建 LLM 的 system prompt)
func (tr *ToolRegistry) GetAvailableTools(ctx context.Context, userID string) []*ToolDefinition {
    var available []*ToolDefinition
    for _, tool := range tr.tools {
        canUse := true
        for _, perm := range tool.RequiredPermissions {
            ok, err := tr.checker.Can(ctx, userID, perm.Action, perm.Resource)
            if err != nil || !ok {
                canUse = false
                break
            }
        }
        if canUse {
            available = append(available, tool)
        }
    }
    return available
}

func (tr *ToolRegistry) registerDefaultTools() {
    tr.RegisterTool(&ToolDefinition{
        Name:        "search_kb",
        Description: "搜索内部知识库",
        RequiredPermissions: []rbac.Permission{
            rbac.NewPermission(rbac.ActionRead, rbac.ResourceKnowledgeBase),
        },
        Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
            query, _ := args["query"].(string)
            return map[string]string{"result": "mock result for: " + query}, nil
        },
    })

    tr.RegisterTool(&ToolDefinition{
        Name:        "create_ticket",
        Description: "创建支持工单",
        RequiredPermissions: []rbac.Permission{
            rbac.NewPermission(rbac.ActionCreate, rbac.ResourceTicket),
        },
        Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
            title, _ := args["title"].(string)
            return map[string]string{"ticket_id": "TKT-001", "title": title}, nil
        },
    })

    tr.RegisterTool(&ToolDefinition{
        Name:        "get_user_info",
        Description: "获取用户详细信息(仅 HR 可用)",
        RequiredPermissions: []rbac.Permission{
            rbac.NewPermission(rbac.ActionRead, rbac.ResourceUserInfo),
        },
        Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) {
            userID, _ := args["user_id"].(string)
            return map[string]string{"user_id": userID, "name": "Alice", "department": "Engineering"}, nil
        },
    })
}

版本5:集成 Retrieval(按权限过滤 Chunks)

// internal/rag/retriever.go
package rag

import (
    "context"
    "your-project/internal/rbac"
)

type Chunk struct {
    ID             string
    Text           string
    Classification string // "public", "internal", "confidential"
    Department     string // "hr", "eng", "all"
}

type SecureRetriever struct {
    baseRetriever Retriever
    checker       *rbac.PolicyChecker
}

type Retriever interface {
    Search(ctx context.Context, query string, topK int) ([]Chunk, error)
}

func NewSecureRetriever(base Retriever, checker *rbac.PolicyChecker) *SecureRetriever {
    return &SecureRetriever{
        baseRetriever: base,
        checker:       checker,
    }
}

// Search 检索后按用户权限过滤
func (r *SecureRetriever) Search(ctx context.Context, userID, query string, topK int) ([]Chunk, error) {
    // 1. 先检查基本的搜索权限
    if err := r.checker.MustCan(ctx, userID, rbac.ActionRead, rbac.ResourceKnowledgeBase); err != nil {
        return nil, err
    }

    // 2. 执行向量搜索(不限权限)
    allChunks, err := r.baseRetriever.Search(ctx, query, topK*3)
    if err != nil {
        return nil, err
    }

    // 3. 按用户权限过滤 chunks
    var filtered []Chunk
    for _, chunk := range allChunks {
        if r.canAccessChunk(ctx, userID, chunk) {
            filtered = append(filtered, chunk)
            if len(filtered) >= topK {
                break
            }
        }
    }

    return filtered, nil
}

// canAccessChunk 检查用户是否有权访问某个 chunk
func (r *SecureRetriever) canAccessChunk(ctx context.Context, userID string, chunk Chunk) bool {
    switch chunk.Classification {
    case "public":
        return true
    case "internal":
        // 需要有 read:chunk 权限(所有内部员工都有)
        ok, _ := r.checker.Can(ctx, userID, rbac.ActionRead, rbac.ResourceChunk)
        return ok
    case "confidential":
        // confidential 内容按部门检查
        if chunk.Department == "hr" {
            ok, _ := r.checker.Can(ctx, userID, rbac.ActionRead, rbac.ResourceUserInfo)
            return ok
        }
        return false
    default:
        return false
    }
}

版本6:HTTP Middleware 集成

// internal/rbac/middleware.go
package rbac

import (
    "encoding/json"
    "errors"
    "net/http"
)

// RequirePermission 返回一个 HTTP middleware,检查特定权限
func RequirePermission(checker *PolicyChecker, action Action, resource Resource) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 从 context 中获取 user_id(由认证 middleware 注入)
            userID, ok := r.Context().Value("user_id").(string)
            if !ok || userID == "" {
                http.Error(w, "unauthorized", http.StatusUnauthorized)
                return
            }

            err := checker.MustCan(r.Context(), userID, action, resource)
            if err != nil {
                var permErr *PermissionDeniedError
                if errors.As(err, &permErr) {
                    w.Header().Set("Content-Type", "application/json")
                    w.WriteHeader(http.StatusForbidden)
                    json.NewEncoder(w).Encode(map[string]string{
                        "error": "permission denied",
                        "detail": permErr.Error(),
                    })
                    return
                }
                http.Error(w, "internal error", http.StatusInternalServerError)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

在路由中使用:

// cmd/api/main.go(路由注册示例)
router.With(
    RequirePermission(checker, ActionCreate, ResourceTicket),
).Post("/api/ticket", ticketHandler.Create)

router.With(
    RequirePermission(checker, ActionRead, ResourceUserInfo),
).Get("/api/users/{id}", userHandler.Get)

完整测试

// internal/rbac/policy_test.go
package rbac

import (
    "context"
    "testing"
)

// MockUserRepository 测试用的 mock
type MockUserRepository struct {
    userRoles map[string][]string
}

func (m *MockUserRepository) GetUserRoles(_ context.Context, userID string) ([]string, error) {
    return m.userRoles[userID], nil
}

func TestPolicyChecker_Can(t *testing.T) {
    registry := NewRegistry()
    repo := &MockUserRepository{
        userRoles: map[string][]string{
            "alice": {"employee"},
            "bob":   {"l2_support"},
            "carol": {"hr"},
            "dave":  {"admin"},
        },
    }
    checker := NewPolicyChecker(registry, repo)
    ctx := context.Background()

    tests := []struct {
        name     string
        userID   string
        action   Action
        resource Resource
        want     bool
    }{
        {"员工可以搜索知识库", "alice", ActionRead, ResourceKnowledgeBase, true},
        {"员工不能创建工单", "alice", ActionCreate, ResourceTicket, false},
        {"L2支持可以审批工单", "bob", ActionApprove, ResourceTicket, true},
        {"L2支持通过继承可以搜索知识库", "bob", ActionRead, ResourceKnowledgeBase, true},
        {"HR可以查用户信息", "carol", ActionRead, ResourceUserInfo, true},
        {"HR不能创建工单", "carol", ActionCreate, ResourceTicket, false},
        {"Admin拥有所有权限", "dave", ActionManage, ResourceRole, true},
        {"Admin通过继承可以审批工单", "dave", ActionApprove, ResourceTicket, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := checker.Can(ctx, tt.userID, tt.action, tt.resource)
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if got != tt.want {
                t.Errorf("Can(%q, %q, %q) = %v, want %v",
                    tt.userID, tt.action, tt.resource, got, tt.want)
            }
        })
    }
}

第三部分:关键概念总结

概念 说明 在哪里用
Permission action:resource 的最小权限单元 注册在 Role 上
Role 一组 Permission 的集合,可继承 赋给 User
Policy Check can(user, action, resource) Tool 执行前、Handler 前
Tool Registry 工具声明所需权限,执行前自动检查 Agent 调用工具时
Secure Retriever 搜索结果按用户权限过滤 RAG 返回 chunks 时
RBAC Middleware HTTP 层自动拦截越权请求 路由注册时

角色继承的好处:

employee → l1_support → l2_support → admin ↗ hr ──

admin 不需要手动列所有权限,通过继承自动获得。


第四部分:自测清单

运行前,问自己:

  • 我能解释 RBAC 相比散落检查的优势?(至少3点)
  • can(user, action, resource) 的完整执行路径是什么?(从 HTTP 请求到 DB 查角色到权限匹配)
  • 角色继承如何防止循环?(visited map)
  • Tool Registry 怎么利用 RBAC 自动限制 LLM 可用的工具?
  • Secure Retriever 过滤的时机是搜索前还是搜索后?(为什么是搜索后?)
  • PermissionDeniedError 为什么要定义成独立类型而不是 errors.New

第五部分:作业

任务1:跑通权限检查

  • 完成 internal/rbac/ 目录下的完整实现
  • 跑通所有测试用例:go test ./internal/rbac/...
  • 添加一个新角色 contractor:只能搜索 public 分类的 chunks

任务2:集成 Tool Registry

  • ExecuteTool 在权限不足时返回清晰的错误信息
  • GetAvailableTools 为不同用户返回不同的工具列表
  • /api/chat handler 中,根据当前用户动态构建 LLM 的工具列表

任务3:集成 Retrieval

  • 实现 SecureRetriever
  • 测试:employee 只看到 public chunks,hr 额外看到 confidential chunks

任务4:深化思考

  • 如果权限规则需要"时间窗口"(比如只有工作时间才能操作),RBAC 如何扩展?
  • **ABAC(Attribute-Based Access Control)**和 RBAC 的区别是什么?什么时候需要 ABAC?
  • **最小权限原则(Principle of Least Privilege)**在你的系统里是否实现了?

第六部分:常见问题

Q: 用户的角色应该存在 JWT 里还是每次查数据库?

A: 两者各有取舍:

  • JWT 存角色:快(无 DB 查询),但角色变更不实时生效(需等 token 过期)
  • 每次查 DB:实时,但有延迟,需缓存优化

建议:用 Redis 缓存用户角色(TTL 5分钟),兼顾性能和实时性:

func (r *CachedUserRepo) GetUserRoles(ctx context.Context, userID string) ([]string, error) {
    key := "user_roles:" + userID
    // 先查 Redis,miss 则查 DB 并回写
    return r.cache.GetOrSet(ctx, key, 5*time.Minute, func() ([]string, error) {
        return r.db.GetUserRoles(ctx, userID)
    })
}

Q: GetAvailableTools 要不要把没权限的工具完全隐藏,还是返回但标记为 disabled?

A: 对 LLM 来说,隐藏比标记更好。因为:

  • LLM 会尝试调用它能"看到"的工具
  • 如果工具存在但被拒绝,会增加 LLM 的困惑和 retry
  • 直接不告诉 LLM 有这个工具,它就不会尝试

Q: 权限矩阵应该硬编码还是存数据库?

A: 分层存储:

  • 基础角色和权限:代码里定义(版本控制,可审计)
  • 用户-角色映射:数据库(动态变更)
  • 资源级别的细粒度权限(如"只能看自己的工单"):数据库

第七部分:算法练习

算法1:Course Schedule II(拓扑排序)

关联:RBAC 的角色继承本质上是一个有向无环图(DAG)。如果出现循环继承(A 继承 B,B 继承 A),系统会死循环。Course Schedule II 用拓扑排序检测是否有循环依赖。

// 课程表 II:返回合法的学习顺序
// 输入:numCourses=4, prerequisites=[[1,0],[2,0],[3,1],[3,2]]
// 输出:[0,1,2,3] 或 [0,2,1,3]

func findOrder(numCourses int, prerequisites [][]int) []int {
    // 构建邻接表和入度表
    graph := make([][]int, numCourses)
    inDegree := make([]int, numCourses)

    for _, pre := range prerequisites {
        course, pre := pre[0], pre[1]
        graph[pre] = append(graph[pre], course)
        inDegree[course]++
    }

    // BFS:Kahn 算法
    queue := []int{}
    for i := 0; i < numCourses; i++ {
        if inDegree[i] == 0 {
            queue = append(queue, i)
        }
    }

    result := []int{}
    for len(queue) > 0 {
        node := queue[0]
        queue = queue[1:]
        result = append(result, node)

        for _, next := range graph[node] {
            inDegree[next]--
            if inDegree[next] == 0 {
                queue = append(queue, next)
            }
        }
    }

    // 如果有环,result 长度会小于 numCourses
    if len(result) != numCourses {
        return []int{} // 有循环依赖,无法完成
    }
    return result
}

与 RBAC 的对应

  • 课程 = 角色
  • 先修关系 = 继承关系
  • 无法完成 = 循环继承(系统应拒绝注册)

算法2:Network Delay Time(Dijkstra)

关联:权限的传播路径。在复杂的角色继承树中,某个权限从根角色传播到叶节点需要经过多少跳?哪个角色是"权限瓶颈"?

// 网络延迟:从节点 k 出发,所有节点都收到信号的最短时间
// 输入:times=[[2,1,1],[2,3,1],[3,4,1]], n=4, k=2
// 输出:2

func networkDelayTime(times [][]int, n int, k int) int {
    // 构建邻接表
    graph := make(map[int][][2]int)
    for _, t := range times {
        u, v, w := t[0], t[1], t[2]
        graph[u] = append(graph[u], [2]int{v, w})
    }

    // Dijkstra
    dist := make([]int, n+1)
    for i := range dist {
        dist[i] = 1<<31 - 1
    }
    dist[k] = 0

    // 最小堆:[distance, node]
    // 用简单的模拟(面试中可以用 container/heap)
    visited := make([]bool, n+1)

    for count := 0; count < n; count++ {
        // 找未访问中距离最小的节点
        u := -1
        for i := 1; i <= n; i++ {
            if !visited[i] && (u == -1 || dist[i] < dist[u]) {
                u = i
            }
        }
        if dist[u] == 1<<31-1 {
            break
        }
        visited[u] = true

        for _, edge := range graph[u] {
            v, w := edge[0], edge[1]
            if dist[u]+w < dist[v] {
                dist[v] = dist[u] + w
            }
        }
    }

    maxDist := 0
    for i := 1; i <= n; i++ {
        if dist[i] == 1<<31-1 {
            return -1
        }
        if dist[i] > maxDist {
            maxDist = dist[i]
        }
    }
    return maxDist
}

复杂度:O(V²)(朴素),O((V+E)logV)(堆优化)


下一步:Day 20 预告

明天我们会:

  1. 实现一个 MCP Server,把今天的 tools 暴露给 Claude Desktop
  2. 理解 MCP Protocol 如何标准化工具调用
  3. 用 stdio transport 实现,让任何支持 MCP 的 LLM 都能调用

准备问题:

  • 如果让不同 LLM(Claude、GPT-4、Gemini)都能调用你的 tools,你现在怎么做?
  • MCP 解决了什么问题?(标准化 vs 重复实现适配层)