Fourth Brother's AI Journey

Post-80s Breakthrough: From Programmer to AI's Fourth Dream

article-7-prompt-engineering-architecture-avoid-over-engineering-traps

Prompt Engineering as Architecture: Don't Let AI Fall Into Over-Engineering Traps

๐ŸŽฏ The Ultimate Paradox of AI Programming: The Smarter You Are, The More Complex Your Code Becomes

In AI programming, there's a counterintuitive phenomenon: the clearer your requirement description, the more complex the code AI generates.

Why? Because AI doesn't understand the cost of dependency maintenance. When it considers all possible edge cases, it falls into the trap of over-engineering, generating "perfect but unmaintainable" code.

During Tidepool Notes development, I summarized a set of "Prompt Engineering as Architecture" methodology, with the core principle: define interfaces first, then talk about implementation.

๐Ÿง  AI's Tendency to Over-Engineer: Why AI Always Overthinks?

1. The Problem of Considering Too Many Edge Cases

AI's Thinking Pattern:

User Requirement: Create a user login feature

AI's Overthinking:
- What if user forgets password? โ†’ Need password reset feature
- What if password is too simple? โ†’ Need password strength validation
- What if someone tries brute force? โ†’ Need login failure lockout
- What if network is unstable? โ†’ Need retry mechanism
- What if server crashes? โ†’ Need circuit breaker
- What if data leaks? โ†’ Need data encryption
- What if...

Result: A simple login feature becomes a complex system with 10 sub-modules.

Reality is: Your mini-program might only have 10 users, you don't need these "what if" scenarios at all.

2. AI Doesn't Understand Maintenance Costs

AI's Design Logic:

  • โœ… High functional completeness
  • โœ… Comprehensive error handling
  • โœ… Good extensibility
  • โŒ Extremely high maintenance cost
  • โŒ Exploding code complexity
  • โŒ Huge learning cost

AI's Blind Spot:

AI thinks: Add one more validation function โ†’ 10 more lines of code โ†’ No big deal

Reality: Add one more validation function โ†’ Need testing โ†’ Need documentation โ†’ Need maintenance โ†’
         Team members need to learn โ†’ Future modifications might be needed โ†’ Technical debt accumulates

3. Real Over-Engineering Cases

Case 1: Simple Message Storage Feature

Your Requirement: "Store user-sent messages"

AI's Over-Design:

// Complex message system generated by AI
interface MessageSystem {
  // Message body
  message: {
    id: string
    content: string
    type: 'text' | 'image' | 'voice' | 'video'
    metadata: {
      device: string
      location: GeoLocation
      sentiment: SentimentAnalysis
      priority: 'low' | 'medium' | 'high' | 'urgent'
    }
  }

  // Message status
  status: {
    delivered: boolean
    read: boolean
    archived: boolean
    deleted: boolean
    retry_count: number
    last_error?: ErrorInfo
  }

  // Version control
  version: {
    current: number
    history: MessageVersion[]
    conflict_resolution: ConflictStrategy
  }

  // Security related
  security: {
    encrypted: boolean
    checksum: string
    access_level: AccessLevel
    audit_trail: AuditEntry[]
  }
}

// Corresponding complex implementation (500+ lines of code)
class ComplexMessageService {
  constructor(
    private messageRepository: MessageRepository,
    private encryptionService: EncryptionService,
    private auditService: AuditService,
    private sentimentService: SentimentService,
    private geoService: GeoService,
    private versionService: VersionService
  ) {}

  async createMessage(input: CreateMessageInput): Promise<MessageSystem> {
    // 50 lines of validation logic
    // 30 lines of encryption processing
    // 40 lines of sentiment analysis
    // 20 lines of geolocation processing
    // 60 lines of version control
    // 80 lines of audit logging
    // ...
  }
}

Simple Design Actually Needed:

// Simple and direct design
interface Message {
  id: string
  sessionId: string
  content: string
  createdAt: Date
}

class MessageService {
  constructor(
    private messageRepository: MessageRepository,
    private logger: Logger
  ) {}

  async createMessage(sessionId: string, content: string): Promise<Message> {
    try {
      const message = await this.messageRepository.create({
        sessionId,
        content,
        createdAt: new Date()
      })
      this.logger.info(`Message created: ${message.id}`)
      return message
    } catch (error) {
      this.logger.exception('Failed to create message', error)
      throw error
    }
  }
}

Comparison Results:

  • Code lines: 500 lines vs 30 lines
  • Number of dependencies: 6 vs 2
  • Maintenance cost: extremely high vs extremely low
  • Function satisfaction: 100% vs 100%

๐Ÿ—๏ธ Interface-First Principle: Define Contract First, Then Implementation

Why Must Define Interfaces First?

Traditional Development Problems:

Developer A: I'll implement user features first
Developer B: I'll implement messaging features first
2 weeks later: Discover interfaces of two modules are incompatible, need refactoring

Advantages of Interface-First:

Product Manager: Let's define all interfaces first
- User service interface
- Message service interface
- AI service interface

Then implement separately, ensuring compatibility

Golden Rules of Interface Design

Rule 1: Interface as Contract

  • Once defined, cannot be changed arbitrarily
  • All implementations must strictly follow
  • Interface changes require team review

Rule 2: Simplicity First

  • As few interfaces as possible
  • As few parameters as possible
  • Return value structure as simple as possible

Rule 3: Single Responsibility

  • Each interface does only one thing
  • Avoid composite operation interfaces
  • Maintain atomicity of interfaces

Interface Design Practice in Tidepool Notes

Step 1: Define Core Interfaces

// types/interfaces.ts - Core interface definitions

// User-related interfaces
interface UserService {
  create(userData: CreateUserInput): Promise<User>
  findById(id: string): Promise<User>
  findByWxCode(wxCode: string): Promise<User>
}

// Session-related interfaces
interface SessionService {
  create(userId: string): Promise<Session>
  findById(id: string): Promise<Session>
  updateStatus(id: string, status: SessionStatus): Promise<void>
}

// Message-related interfaces
interface MessageService {
  create(sessionId: string, content: string): Promise<Message>
  findBySessionId(sessionId: string): Promise<Message[]>
}

// AI service-related interfaces
interface LLMService {
  generateSuggestion(sessionData: SessionData): Promise<string>
}

Step 2: Design Prompts Based on Interfaces

// Prompt template: Constrain AI implementation based on interfaces
const promptTemplate = `
Please implement the corresponding service class based on the following interface definitions:

Interface Definitions:
${interfaceDefinition}

Constraint Conditions:
1. Strictly follow interface signatures, cannot change parameters and return values
2. Use TypeScript strict mode, prohibit any types
3. Function implementation not exceeding 50 lines
4. Error handling uses logger.exception + throw
5. Do not add features not defined in interfaces

Technology Stack:
- Database: Prisma + PostgreSQL
- Logging: Winston
- Validation: Simple if judgments, no additional libraries

Please implement the above interfaces.
`

Step 3: Constrain AI Behavior with Interfaces

// AI-generated implementation must comply with interface constraints
export class UserServiceImpl implements UserService {
  constructor(
    private userRepository: UserRepository,
    private logger: Logger
  ) {}

  // Strictly follow interface definition
  async create(userData: CreateUserInput): Promise<User> {
    try {
      const user = await this.userRepository.create(userData)
      this.logger.info(`User created: ${user.id}`)
      return user
    } catch (error) {
      this.logger.exception('Failed to create user', error)
      throw error
    }
  }

  // Cannot add methods not defined in interface
  // โŒ async validateComplexRules(): void { ... }
  // โŒ async sendWelcomeEmail(): void { ... }
  // โŒ async createBackup(): void { ... }
}

๐ŸŽฏ Architectural Methodology of Prompt Engineering

1. Three-Layer Prompt Architecture

First Layer: Requirement Layer (What)

Clearly state what to do, not how to do it

Examples:
โŒ Don't say: Implement an enterprise-grade user authentication system
โœ… Say: Implement user registration and login features

โŒ Don't say: Design a high-performance message queue
โœ… Say: Implement message sending and receiving

Second Layer: Constraint Layer (How Not)

Clearly define boundaries and limitations, prevent AI from over-expressing

Examples:
Constraint Conditions:
- Single function not exceeding 50 lines
- Do not use third-party libraries (except database ORM)
- Throw errors directly, do not provide fallback solutions
- Interfaces cannot be changed, implementations must strictly follow
- Do not consider future extensions, focus on current needs

Third Layer: Implementation Layer (How)

Let AI freely express within constraint range

Examples:
Technical Requirements:
- Use TypeScript strict mode
- Use Prisma for database operations
- Use Winston for logging
- Follow Repository pattern

2. Prompt Templates to Prevent Over-Engineering

Template 1: Feature Constraint Template

I want to implement [feature name], please strictly follow the following requirements:

## Interface Definitions (Unchangeable)
[Specific TypeScript interfaces]

## Business Rules (Must Follow)
1. Only implement features defined in interfaces, do not add extra features
2. Do not consider possible future expansion needs
3. Do not handle extreme edge cases
4. Throw errors directly, do not provide fallback solutions

## Technical Constraints (Mandatory Execution)
1. Single function not exceeding 50 lines
2. Do not use any types
3. Error handling: logger.exception + throw
4. Do not add new dependency packages
5. Keep code simple and direct

## Implementation Requirements
Please implement the corresponding class based on the above interfaces and constraints.

Template 2: Interface-First Template

We need to implement [module name], please design interfaces first, then implement features:

## Step 1: Interface Design
Please design interfaces that meet the following requirements:
- Function description: [Specific feature description]
- Input parameters: [Parameter list]
- Output requirements: [Return value description]
- Edge cases: [Normal situations to consider]

## Step 2: Implementation Constraints
After interface design is complete, I will confirm interface definitions, then ask you to:
1. Strictly implement based on interfaces
2. Do not add features not defined in interfaces
3. Keep code simple
4. Focus on core business logic

Please design interfaces first, do not implement code directly.

Template 3: KISS Constraint Template

Implement [feature name], must follow KISS principle:

## Prohibited Actions
โŒ Do not consider future extensions
โŒ Do not handle extreme edge cases
โŒ Do not use complex design patterns
โŒ Do not add extra configurations and options
โŒ Do not provide fallback solutions
โŒ Do not use third-party validation libraries

## Required Actions
โœ… Directly implement core features
โœ… Use simple if judgments for validation
โœ… Throw errors directly
โœ… Keep code readable
โœ… Focus on current needs

## Reference Standards
If code exceeds 200 lines, it's over-engineered
If functions exceed 10, it's over-engineered
If configuration files are needed, it's over-engineered

Please implement the simplest version.

3. Prompt Strategy for Dependency Maintenance Costs

Strategy 1: Dependency Number Limitation

During implementation, please follow dependency minimization principle:

## Allowed Dependencies
- Database ORM: Prisma
- Logging: Winston
- Testing Framework: Jest
- Node.js built-in modules

## Prohibited Dependencies
- Any utility libraries (lodash, etc.)
- Any validation libraries (yup, zod, joi, etc.)
- Any HTTP client libraries (axios, etc.)
- Any state management libraries

## Dependency Selection Principle
If it can be implemented with native APIs, never use third-party libraries
If it can be solved with simple solutions, never use complex solutions

If additional dependencies are needed, must provide sufficient reasons.

Strategy 2: Complexity Control

Specific requirements to control code complexity:

## Function Complexity
- Single function not exceeding 50 lines
- Nesting level not exceeding 3 layers
- Single function parameters not exceeding 5

## Class Complexity
- Single class not exceeding 300 lines
- Number of methods not exceeding 10
- Dependency injection not exceeding 5

## File Complexity
- Single file not exceeding 500 lines
- Imported modules not exceeding 10
- Exported interfaces not exceeding 5

If exceeding above limits, please redesign simplified solution.

๐ŸŽช Practical Cases: Taming AI with Interface-First

Case 1: Interface-First Implementation of User System

Wrong Approach: Let AI Implement Directly

You: Help me implement a user system

AI (Over-Engineered):
- User registration, login, password reset
- Permission management, role assignment
- Operation auditing, login logs
- Email verification, phone verification
- Account locking, security policies
- User profiling, behavior analysis
- ...
Result: 800 lines of code, 10 dependencies, overly complex

Correct Approach: Interface-First

You: Let's define interfaces for the user system first

You: I need the following features:
1. User registration (username, password)
2. User login (username, password)
3. Get user info (user ID)

Please design TypeScript interfaces first, don't rush to implement code

AI (Interface Design):
interface UserService {
  register(username: string, password: string): Promise<User>
  login(username: string, password: string): Promise<User>
  getUserById(userId: string): Promise<User>
}

You: Interface is good, now implement features based on this interface, constraint conditions:
- Single function not exceeding 30 lines
- Do not use any validation libraries
- Throw errors directly
- Do not add extra features

AI (Simple Implementation):
export class UserServiceImpl implements UserService {
  constructor(
    private userRepository: UserRepository,
    private logger: Logger
  ) {}

  async register(username: string, password: string): Promise<User> {
    if (!username || !password) {
      throw new Error('Username and password required')
    }
    try {
      const user = await this.userRepository.create({ username, password })
      this.logger.info(`User registered: ${user.id}`)
      return user
    } catch (error) {
      this.logger.exception('Registration failed', error)
      throw error
    }
  }

  async login(username: string, password: string): Promise<User> {
    const user = await this.userRepository.findByUsername(username)
    if (!user || user.password !== password) {
      throw new Error('Invalid credentials')
    }
    return user
  }

  async getUserById(userId: string): Promise<User> {
    const user = await this.userRepository.findById(userId)
    if (!user) {
      throw new Error('User not found')
    }
    return user
  }
}

Result: 80 lines of code, 2 dependencies, simple and clear

Case 2: Constrained Implementation of AI Integration Features

Problem: AI tends to over-engineer LLM integration

AI's Over-Engineering Tendency:

// Complex LLM integration that AI wants
class ComplexLLMService {
  constructor() {
    this.promptManager = new PromptManager()
    this.conversationManager = new ConversationManager()
    this.responseParser = new ResponseParser()
    this.cacheManager = new CacheManager()
    this.rateLimiter = new RateLimiter()
    this.errorHandler = new ErrorHandler()
    this.metricsCollector = new MetricsCollector()
  }

  async generateResponse(input: UserInput): Promise<AIResponse> {
    // 100 lines of complex logic
    // Including retry, caching, rate limiting, monitoring, etc.
  }
}

Interface-First Constraint Implementation:

You: We need to integrate AI conversation features, please design interfaces first

AI: interface LLMService {
  generateSuggestion(sessionMessages: Message[]): Promise<string>
}

You: Good, now implement this interface, constraint conditions:
- Only call OpenAI API, do not handle other service providers
- No caching, direct API calls
- No retry, throw error directly on failure
- Function not exceeding 30 lines

AI (Constrained Implementation):
export class LLMServiceImpl implements LLMService {
  constructor(
    private openaiClient: OpenAIClient,
    private logger: Logger
  ) {}

  async generateSuggestion(sessionMessages: Message[]): Promise<string> {
    try {
      const messages = sessionMessages.map(msg => ({
        role: msg.isUser ? 'user' : 'assistant',
        content: msg.content
      }))

      const response = await this.openaiClient.chat.completions.create({
        model: 'gpt-4',
        messages: messages,
        temperature: 0.7,
        max_tokens: 1000
      })

      return response.choices[0].message.content
    } catch (error) {
      this.logger.exception('Failed to generate suggestion', error)
      throw error
    }
  }
}

Case 3: Preventing AI from Adding Unnecessary Features

Common Scenarios: AI likes to "thoughtfully" add extra features

You: Implement message saving functionality

AI (Might add extra features):
- โœ… Message saving
- โŒ Message search (you didn't ask for this)
- โŒ Message categorization (you didn't ask for this)
- โŒ Message statistics (you didn't ask for this)
- โŒ Message export (you didn't ask for this)
- โŒ Message encryption (you didn't ask for this)

Preventive Prompts:

Please implement message saving functionality, strictly follow the following requirements:

## Only Implement Features
- Save messages to database
- Query messages by session ID

## Absolutely Do Not Add Features
โŒ Message search
โŒ Message categorization
โŒ Message statistics
โŒ Message export
โŒ Message encryption
โŒ Any other features not explicitly requested

## Interface Definitions
interface MessageService {
  save(sessionId: string, content: string): Promise<Message>
  getBySessionId(sessionId: string): Promise<Message[]>
}

Please implement strictly according to the above interface, do not add any extra features.

๐ŸŒŸ Summary: Core Principles of Prompt Engineering as Architecture

Three Core Understandings

1. AI Doesn't Understand Maintenance Costs

  • AI pursues functional completeness, humans pursue maintainability
  • AI thinks "one more feature is no big deal," humans know "one more feature means more maintenance burden"
  • Must use prompts to set maintenance cost constraints for AI

2. Interface as Architecture, Constraint as Quality

  • Define interfaces first, then talk about implementation
  • Use interfaces to constrain AI's behavior boundaries
  • Use constraint conditions to control code complexity

3. Simplicity is the Highest Form of Complexity

  • If it can be solved with 10 lines, never use 100 lines
  • If it can be done with 1 interface, never use 3 interfaces
  • If it can be implemented directly, never take detours

Practical Checklist

Prompt Design Check:

  • Are interfaces clearly defined?
  • Are clear constraint conditions set?
  • Is functional scope limited?
  • Are unnecessary features prohibited?
  • Is complexity controlled?

AI Output Check:

  • Does it strictly follow interface definitions?
  • Are there extra features not defined in interfaces?
  • Are code lines reasonable?
  • Is number of dependencies minimized?
  • Does it over-consider edge cases?

Architecture Quality Check:

  • Is interface design simple and clear?
  • Is implementation direct and simple?
  • Is maintenance cost controllable?
  • Is new team member understanding cost low?
  • Are extensions within reasonable scope?

Remember These Golden Sentences

About AI's Nature:

  • "AI is smart, but has no responsibility to maintain code"
  • "AI pursues perfection, but reality needs 'good enough'"
  • "AI doesn't know the human cost behind every feature"

About Architectural Thinking:

  • "Interface as contract, agreement as quality"
  • "Constraints are not limitations, but quality guarantees"
  • "Simplicity is not crudeness, but the highest form of complexity"

About Practice Principles:

  • "First talk about what to do, then how to do it"
  • "First define interfaces, then write implementations"
  • "First constrain boundaries, then unleash creativity"

Ultimately Remember: Good prompt engineering is to let AI showcase its talents on the right track, not let it run wild creating complexity.


Next Article Preview: Cognitive Traps in AI Programming: Why You Think It's Simple, AI Finds It Complex


Search "ๅ››ๅ“ฅ่ฟ˜ไผš่ŠAI" on WeChat to see how these prompt engineering techniques create value in actual projects

Search "ๆฑๅฑฟ็ฌ”่ฎฐ" on WeChat to see how these architectural principles are applied in real projects

โ† Back to Home