[{"content":"AI handles the \u0026ldquo;speed,\u0026rdquo; humans handle the \u0026ldquo;blueprints.\u0026rdquo;\nThis column outlines common software architecture design techniques, aiming to teach you how to tame AI to write industrial-grade, highly maintainable, high-quality code through \u0026ldquo;architectural design\u0026rdquo; and \u0026ldquo;rule constraints (Prompt / Rules),\u0026rdquo; avoiding the pitfall of \u0026ldquo;the more code you write, the faster the system crashes.\u0026rdquo;\nTable of Contents Prologue: The More You Rely on AI to Write Code, the Less You Can Afford to Skip Architecture Design Preliminary: How to Define Coding Boundaries for AI Using Project Rules Section 1: Top-Level Architecture Breakdown Frontend-Backend Separation: Constraining AI\u0026rsquo;s Division of Labor to Avoid Interface Coupling and Responsibility Confusion Domain-Driven Design: Drawing Context Boundaries for AI to Say Goodbye to \u0026quot;Big Ball of Mud\u0026quot; Code Layered Clean Architecture: Standardizing Project Directory Structure to Prevent AI Boundary Violations Exception and Unified Encapsulation Architecture: Standardizing Unified Return Values, Global Exception Handling, and Parameter Validation Section 2: Code Decoupling and Interface Specification Dependency Injection: Forbid AI from Hardcoding Instantiation, Improve Testability and Extensibility Logging Architecture: Making Every Log Line AI Writes Valuable RESTful and Contract First: Standardizing Interface Definitions and Unifying API Design Paradigms Aspect-Oriented Programming: Guiding AI to Extract Cross-Cutting Logic So Logging, Auth, and Instrumentation Are Never Written Twice Section 3: Security, Storage, and Engineering Infrastructure Unified Authentication and Authorization: Standardizing AI Token Issuance and Verification Logic Caching and Multi-Level Storage: Don\u0026rsquo;t Let AI Blindly Hit the Database Multi-Data-Source Architecture: Stop Letting AI Lock Your Project to One Database Centralized Configuration and I18n: Strictly Forbid AI Magic Values and Hardcoded Parameters Section 4: High-Performance Communication and Expansion Architecture Async and Concurrency Architecture: Fixing AI\u0026rsquo;s Synchronous Blocking Patterns to Improve System Throughput Long-Lived Connections and Streaming Push: Standardizing SSE / WebSocket Implementations to Replace Inefficient Polling Plugin Architecture and Extension Points: Guiding AI Toward Modular, Pluggable Development for Decoupled, Iterable Features Architect + AI: The Career Leap from Coder to System Orchestrator Open source code github gitee 🪐 Best of luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/01.%E6%9E%B6%E6%9E%84%E7%AD%91%E5%9F%BA/","summary":"\u003cp\u003e\u003cstrong\u003eAI handles the \u0026ldquo;speed,\u0026rdquo; humans handle the \u0026ldquo;blueprints.\u0026rdquo;\u003c/strong\u003e\u003cbr\u003e\nThis column outlines common software architecture design techniques, aiming to teach you how to tame AI to write industrial-grade, highly maintainable, high-quality code through \u0026ldquo;architectural design\u0026rdquo; and \u0026ldquo;rule constraints (Prompt / Rules),\u0026rdquo; avoiding the pitfall of \u0026ldquo;the more code you write, the faster the system crashes.\u0026rdquo;\u003c/p\u003e","title":"[Collection] Architectural Foundations: The Underlying Methodology for Mastering AI-Assisted Programming"},{"content":"In this series, we have discussed a range of engineering practices: frontend-backend separation, DDD (Domain-Driven Design), layered architecture, DI (Dependency Injection), RESTful (Representational State Transfer), async \u0026amp; concurrency, SSE (Server-Sent Events), plugin architecture, and Project Rules.\nIt may look like a lot of technology, but they all ultimately point to the same question:\nNow that AI can generate large amounts of code, what is still the most important capability for a programmer?\nThe answer is shifting from:\n\u0026ldquo;How to write the code\u0026rdquo;\nto:\n\u0026ldquo;How to design a system in which AI can write code correctly.\u0026rdquo;\n1. Code Is Getting Cheaper, Design Is Getting More Expensive The traditional software development flow roughly looked like:\nUnderstand requirements ↓ Design the solution ↓ Write code ↓ Test and debug Coding took up a large share of the time. Now, AI can quickly complete:\nCRUD API SQL Data models Unit tests Business modules The better AI gets at writing code, the more important one question becomes:\nWhat should be written, and where should that code live?\n2. Establishing Boundaries Looking back at the previous articles, there is a common theme behind all of them:\nFrontend-backend separation → System boundary DDD → Business boundary Layered architecture → Responsibility boundary Repository → Data access boundary DI → Dependency boundary RESTful + contract-first → Interface boundary Async \u0026amp; concurrency → Execution model boundary SSE / WebSocket → Real-time communication boundary Plugin / Extension Point → Feature extension boundary Project Rules → Telling AI about all these boundaries So the purpose of architecture is not to add complexity.\nQuite the opposite:\nArchitecture prescribes where complexity should be contained, before the complexity even appears.\n3. AI Is Changing Where Programmers Focus The traditional development model is closer to:\nRequirements ↓ Programmer designs ↓ Programmer codes ↓ Programmer debugs AI-assisted programming is increasingly closer to:\nRequirements ↓ Architecture design ↓ Define boundaries and contracts ↓ AI implements ↓ Automated validation ↓ Human acceptance Programmers aren\u0026rsquo;t disappearing from the development flow — they are moving further upstream.\nIn the past, we were more like:\nConstruction workers.\nIn the future, increasingly like:\nArchitect + Project Manager + Acceptance Engineer.\nYou may not lay every brick yourself, but you must know:\nHow the house should be designed Where the load-bearing walls are How different modules connect What standards the construction crew must follow What criteria to accept against at the end AI is responsible for speeding up construction.\nHumans are responsible for ensuring:\nThe construction is heading in the right direction.\n4. Upgrading from \u0026ldquo;Writing Code\u0026rdquo; to \u0026ldquo;Designing Systems\u0026rdquo; Developer capability can be roughly understood as several levels:\nLevel 1 Can write code ↓ Level 2 Can implement features ↓ Level 3 Can design modules ↓ Level 4 Can design systems ↓ Level 5 Can design the rules that let AI continuously build the system AI is strongest at replacing low-level coding work.\nBut the higher you go, the more different the problems to solve become:\nHow should the domain be divided? Where are the module boundaries? Which capabilities should be abstracted? Which parts should be plugin-ized? Who is responsible for the data? How should modules depend on each other? Can it still be maintained if the feature set grows tenfold? These are not \u0026ldquo;code generation\u0026rdquo; problems.\nThey are:\nArchitecture problems.\nSo AI has not diminished the value of architecture skills.\nQuite the opposite:\nAI has turned architecture skills into an even bigger lever of productivity.\n5. The Future\u0026rsquo;s More Efficient Model: Humans Design, AI Implements The future software development flow is likely to converge on:\nflowchart LR A[\"Business Requirement\"] --\u003e B[\"Architecture\"] B --\u003e C[\"Rules \u0026 Contracts\"] C --\u003e D[\"AI Coding Agent\"] D --\u003e E[\"Implementation\"] E --\u003e F[\"Test / CI\"] F --\u003e G[\"Human Review\"] G --\u003e H[\"Production\"] The focus of human work is gradually shifting from:\nImplementation\nto:\nDesign + Constrain + Validate.\nThis is also why the whole series has kept emphasizing Project Rules.\nIn the past, architecture specifications might have been just documents. Now they can become:\nArchitecture ↓ Project Rules ↓ AI Context ↓ Generated Code In other words:\nFor the first time, architecture specifications can participate directly in code production.\n6. Good Architecture Paves the Track for AI AI\u0026rsquo;s advantage is speed; architecture\u0026rsquo;s role is direction.\nWithout architecture:\nRequirements ↓ AI improvises freely ↓ More and more code ↓ Structure gets messier and messier With architecture:\nRequirements ↓ Architecture ↓ Rules / Contracts ↓ AI ↓ Implementation that fits the boundaries So good architecture doesn\u0026rsquo;t restrict AI\u0026rsquo;s capabilities — it:\nKeeps AI\u0026rsquo;s high-speed execution on the right track.\nJust as a high-speed train doesn\u0026rsquo;t really need more freedom to roam, but a clear, stable track system.\n7. miniagent Is Also Such a Practice This series has consistently used miniagent as a case study, not to present some so-called \u0026ldquo;standard architecture.\u0026rdquo;\nIt\u0026rsquo;s more like an experiment:\nCan we first establish clear architectural boundaries, then let AI keep developing within those boundaries?\nFor example, miniagent organizes core components — database, Repository, Service, Registry, Factory — through ServiceContainer, and initializes the relevant capabilities at application startup.\nWhat\u0026rsquo;s truly worth paying attention to is not the specific class names, but the development approach behind them:\nDesign first ↓ Constrain ↓ Let AI implement ↓ Test and review ↓ Keep evolving This loop is the engineering capability genuinely worth building for AI programming.\n8. From \u0026ldquo;Coder\u0026rdquo; to \u0026ldquo;System Orchestrator\u0026rdquo; In the AI era, competing with AI on:\nWho writes code faster?\nmatters less and less.\nWhat developers should more worthily improve is:\nUnderstanding the business ↓ Abstracting the domain ↓ Designing the architecture ↓ Defining boundaries ↓ Setting the rules ↓ Breaking down tasks ↓ Directing AI ↓ Validating the results And so the role shifts from:\nCoder\ngradually to:\nSystem Orchestrator.\nThe outstanding developer of the future may not be the one who writes the most code on the team.\nMore likely, it\u0026rsquo;s the one who can:\nTurn complex business into a clear system, turn the system into explicit rules, and keep AI working according to those rules.\nConclusion If the entire series were compressed into one sentence, it would be:\nDon\u0026rsquo;t just learn how to make AI write code for you — learn how to design a system worth having AI write.\nWhen the focus shifts from:\n\u0026ldquo;How should this function be written?\u0026rdquo;\ngradually to:\n\u0026ldquo;Should this module exist? Where are its boundaries? What is the interface? Who depends on whom? Where is extension allowed? What rules should AI follow?\u0026rdquo;\nthe role has already begun to change:\nCoder ↓ Architect ↓ AI Orchestrator From a producer of code,\ntoward the commander of the system.\nOpen Source Code github gitee 🪐 Wishing you good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0118/","summary":"\u003cp\u003eIn this series, we have discussed a range of engineering practices: frontend-backend separation, DDD (Domain-Driven Design), layered architecture, DI (Dependency Injection), RESTful (Representational State Transfer), async \u0026amp; concurrency, SSE (Server-Sent Events), plugin architecture, and Project Rules.\u003c/p\u003e\n\u003cp\u003eIt may look like a lot of technology, but they all ultimately point to the same question:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eNow that AI can generate large amounts of code, what is still the most important capability for a programmer?\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe answer is shifting from:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;How to write the code\u0026rdquo;\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eto:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;How to design a system in which AI can write code correctly.\u0026rdquo;\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e","title":"Architect + AI: The Career Leap from Coder to System Orchestrator"},{"content":"AI writes code fast, but it is also prone to a typical problem:\nEvery time a feature is added, more if/elif branches pile into the core code.\nFor example:\nif domain == \u0026#34;legal\u0026#34;: ... elif domain == \u0026#34;finance\u0026#34;: ... elif domain == \u0026#34;medical\u0026#34;: ... With few features this is fine, but as the types keep growing, the core module becomes increasingly bloated, and every new feature risks affecting existing logic.\nA more reasonable approach is to define upfront:\nPlugin Extension Point\nAnd let AI follow one principle:\nNew capabilities should preferably be added as new modules, not by modifying the core flow.\n📌 Technical Profile Plugin Architecture\nSplit easily-changing features into independent modules that plug into the system through a unified interface. The core system depends on interfaces, not on any specific implementation.\nAmong these:\nExtension Point\nA pre-reserved entry point for features to plug into the system.\nFor example:\nflowchart TD A[\"Core System\"] --\u003e B[\"Extension Point\"] B --\u003e C[\"LegalPlugin\"] B --\u003e D[\"FinancePlugin\"] B --\u003e E[\"MedicalPlugin\"] The core system doesn\u0026rsquo;t need to know how each plugin is implemented internally — it only needs to know:\nThey all follow the same interface.\n💡 A Simple Analogy Plugin architecture is a lot like a computer\u0026rsquo;s USB port.\nThe computer doesn\u0026rsquo;t need a different machine for the mouse, the keyboard, and the USB drive — it just defines a unified USB interface.\nSoftware architecture works the same way:\nThe core system defines \u0026ldquo;how to plug in\u0026rdquo;; the plugin decides \u0026ldquo;what to do once plugged in.\u0026rdquo;\n1. Why Does AI Especially Need Extension Points? Suppose the knowledge base currently supports:\ngeneral legal If you ask AI to add a finance domain, it will most likely write:\nif domain == \u0026#34;general\u0026#34;: return process_general(document) elif domain == \u0026#34;legal\u0026#34;: return process_legal(document) elif domain == \u0026#34;finance\u0026#34;: return process_finance(document) Next time you add a medical domain, another branch gets added.\nEventually:\nCore Service ↓ Lots of if / elif ↓ All domain logic coupled together A better way of thinking:\nCore ↓ Plugin Interface ↓ Registry ↓ Concrete Plugin This follows the:\nOpen-Closed Principle\nPut simply:\nOpen for extension, closed for modification.\nAdd features by adding new implementations, rather than repeatedly modifying already-stable core code.\n2. Architectural Principles 1. Interface → Plugin → Registry Plugin architecture can be summarized into three core roles.\nInterface First, define what capabilities a plugin must provide:\nfrom abc import ABC, abstractmethod class DomainPlugin(ABC): @abstractmethod def parse_metadata(self, raw: dict) -\u0026gt; dict: pass Here:\nABC (Abstract Base Class)\nacts as the plugin contract.\nConcrete plugins just implement it:\nflowchart TD A[\"DomainPlugin\"] A --\u003e B[\"LegalPlugin\"] A --\u003e C[\"FinancePlugin\"] A --\u003e D[\"MedicalPlugin\"] The core system depends only on DomainPlugin, not on any specific domain.\nRegistry The system also needs to know:\nWhich plugin should be used for a given domain?\nThis can be achieved with:\nRegistry Pattern\nimplemented as:\nclass PluginRegistry: def __init__(self): self._plugins = {} def register(self, name, plugin): self._plugins[name] = plugin def get(self, name): return self._plugins.get(name) At runtime you only need:\nplugin = registry.get(domain) instead of:\nif domain == ... Loader Plugin creation should preferably not be scattered across business code either.\nRecommended:\nApplication Startup ↓ Plugin Loader ↓ Plugin Registry At runtime:\nRequest ↓ Registry.get() ↓ Plugin In other words:\nThe startup phase is responsible for discovery and registration; the runtime phase is responsible for lookup and use.\n2. Going Further with Configuration: Dynamic Plugin Loading If plugins will be added frequently in the future, you can make the concrete implementations configurable.\nFor example, store in the configuration:\napp.plugins.legal.LegalPlugin The system loads it dynamically via:\nmodule = importlib.import_module(module_path) plugin_cls = getattr(module, class_name) This way, the core system doesn\u0026rsquo;t need to write:\nfrom app.plugins.legal import LegalPlugin from app.plugins.finance import FinancePlugin from app.plugins.medical import MedicalPlugin Adding a plugin becomes:\nImplement Plugin + Add Configuration ↓ System loads it automatically This is:\nPlug-and-Play.\n3. What Features Are Suitable as Extension Points? Not all code needs to be plugin-ized.\nWhat genuinely deserves an extension point is usually:\nA part of a stable flow that has multiple implementations and will keep changing in the future.\nCommon extension points in Agent systems include:\nCore Runtime │ ├── Tool Plugin ├── Domain Plugin ├── Retriever ├── Reranker ├── LLM Provider ├── Vector Store └── Document Processor Among them:\nRetriever Reranker LLM (Large Language Model) Provider Vector Store may all have multiple implementations.\nA very practical heuristic:\nIf a place starts accumulating more and more if type == \u0026quot;A\u0026quot;, elif type == \u0026quot;B\u0026quot; branches, check whether it\u0026rsquo;s missing an extension point.\nBut for simple, stable logic, there\u0026rsquo;s no need to force in Plugin, Registry, and Factory — otherwise you end up with:\nOverengineering.\n3. What Does Plugin Architecture Bring? The most direct change:\nTraditional approach New feature ↓ Modify Core ↓ Re-validate core flow becomes:\nPlugin approach New feature ↓ Implement Plugin ↓ Register This brings several practical benefits:\nLower coupling: domain logic doesn\u0026rsquo;t all pile into the core Service; Lower regression risk: new plugins rarely affect existing ones; Easier testing: each plugin can be tested independently; Easier swapping: implementations can be switched via configuration; Easier AI development: AI knows where a new feature should go. The last point matters most.\nWithout extension points, AI\u0026rsquo;s development pattern tends to be:\nFind old code ↓ Insert new condition ↓ Modify multiple Services With extension points, it becomes:\nFind the Extension Point ↓ Implement Plugin ↓ Register Plugin architecture essentially draws a boundary for AI:\nThe correct direction for feature growth.\n4. Putting Prompts into Practice: Explicitly Telling AI How to Extend You can put the following rules into:\nProject Rules\n## Plugin and Extension Rules 1. Prefer extension points over adding type-based if/elif branches to core services. 2. Core modules must depend on abstractions, not concrete plugin implementations. 3. Extensible features should follow this flow: Interface → Plugin Implementation → Registry → Runtime Lookup. 4. Do not instantiate concrete plugins inside business services. 5. Plugin discovery and registration should happen during application startup. 6. Domain-specific behavior must stay inside plugins and must not leak into generic services. 7. Prefer configuration-driven registration when implementations can change independently. 8. Adding a plugin should ideally only require: - The plugin implementation - Registration/configuration - Plugin tests 9. Do not introduce plugin abstractions when multiple implementations are unlikely. 10. Before adding a type-based if/elif branch, check whether that spot should be an extension point. The core purpose is not to demand:\n\u0026ldquo;Write a few more Plugin classes.\u0026rdquo;\nbut rather:\nStable capabilities stay in the Core; changeable capabilities go into Extension Points.\n5. A Positive Example: miniagent\u0026rsquo;s Domain Plugin Mechanism In miniagent\u0026rsquo;s knowledge base system, there is a typical extension point:\nDomain Plugin\nDifferent domains can have their own:\nDocument metadata processing; Small-to-Big context expansion; Citation merging rules; while the generic knowledge base flow doesn\u0026rsquo;t need to know the specifics of each domain.\n1. DomainPlugin Defines the Plugin Contract miniagent defines:\nclass DomainPlugin(ABC): @property @abstractmethod def processor(self) -\u0026gt; SmallToBigProcessor: ... @abstractmethod def parse_metadata(self, raw: dict) -\u0026gt; dict: ... @property def citation_merger(self) -\u0026gt; CitationMerger: return CitationMerger() It defines several domain extension points:\nDomainPlugin │ ├── processor ├── parse_metadata() └── citation_merger A new domain only needs to implement this contract, without shoving domain-specific branching into the generic flow.\n2. DomainRegistry Manages Plugins Centrally miniagent uses:\nclass DomainRegistry: def __init__(self): self._plugins: dict[str, DomainPlugin] = {} def register( self, domain: str, plugin: DomainPlugin ) -\u0026gt; None: self._plugins[domain] = plugin def get(self, domain: str) -\u0026gt; DomainPlugin: return self._plugins.get(domain) The runtime relationship therefore becomes:\ndomain ↓ DomainRegistry ↓ DomainPlugin instead of an ever-growing if/elif chain.\n3. Loading Concrete Implementations Dynamically from the Database Going one step further, miniagent reads the domain configuration at ServiceContainer startup:\ndomains = await self.domain_db.get_all_domains() Then, based on the configuration\u0026rsquo;s:\nprocessor_class plugin_class it loads them dynamically:\nprocessor_cls = import_class( domain_orm.processor_class ) processor_instance = processor_cls() plugin_cls = import_class( domain_orm.plugin_class ) plugin_instance = plugin_cls( processor=processor_instance ) Finally, it registers:\nself.domain_registry.register( domain=domain_orm.name, plugin=plugin_instance ) The core of the dynamic import is:\ndef import_class(class_path: str): module_path, class_name = class_path.rsplit(\u0026#34;.\u0026#34;, 1) module = importlib.import_module(module_path) return getattr(module, class_name) So the whole plugin loading process can be summarized as:\nDatabase Configuration ↓ processor_class / plugin_class ↓ Dynamic Import ↓ Create Plugin ↓ DomainRegistry.register() ↓ Runtime Lookup The diagram below describes miniagent\u0026rsquo;s domain plugin implementation mechanism in detail:\nflowchart TD A[\"Application Startup\"] --\u003e B[\"Load Domain Config\"] B --\u003e C[\"processor_class / plugin_class\"] C --\u003e D[\"Dynamic Import\"] D --\u003e E[\"Create DomainPlugin\"] E --\u003e F[\"DomainRegistry.register()\"] G[\"Runtime Request\"] --\u003e H[\"domain\"] H --\u003e I[\"DomainRegistry.get(domain)\"] I --\u003e E E --\u003e J[\"Domain-specific Behavior\"] What matters most here are two phases:\nStartup phase Discover → Create → Register plugins Runtime phase Lookup → Use plugins Plugin management and business execution are therefore separated.\n4. What Happens When a New Domain Is Added? Suppose miniagent adds a finance domain in the future.\nThe ideal path is:\nFinance Processor ↓ Finance DomainPlugin ↓ Add Domain configuration ↓ System auto-loads at startup ↓ DomainRegistry instead of:\nModify Retrieval Service Modify Document Service Add finance if/elif Modify core routes This is exactly the effect plugin architecture hopes to achieve:\nExtend the system by adding implementations, not by modifying the core flow.\nSummary AI is very good at adding more logic to existing code, but software that evolves over the long term needs:\nStable Core ↓ Extension Point ↓ Plugin The plugin principle can be compressed into four statements:\nStable capabilities → stay in the Core Changeable capabilities → go into Plugins Plugin management → handled by the Registry Concrete implementations → wired in via configuration miniagent\u0026rsquo;s DomainPlugin + DomainRegistry + dynamic import + database configuration embodies this approach: the interface defines the extension boundary, the Registry manages concrete implementations, and ServiceContainer discovers and registers plugins dynamically at startup.\nFor AI programming, the single most valuable sentence to put in your project rules is:\nWhen adding a new capability, first look for an extension point; if one exists, add a new implementation instead of modifying the core flow.\nThis way, AI isn\u0026rsquo;t endlessly \u0026ldquo;piling code\u0026rdquo; into the system, but inserting into the existing architecture a:\nModule that is replaceable, testable, and independently iterable.\nOpen Source Code github gitee 🪐 Wishing you good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0117/","summary":"\u003cp\u003eAI writes code fast, but it is also prone to a typical problem:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eEvery time a feature is added, more \u003ccode\u003eif/elif\u003c/code\u003e branches pile into the core code.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eFor example:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003edomain\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;legal\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eelif\u003c/span\u003e \u003cspan class=\"n\"\u003edomain\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;finance\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eelif\u003c/span\u003e \u003cspan class=\"n\"\u003edomain\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;medical\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"o\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWith few features this is fine, but as the types keep growing, the core module becomes increasingly bloated, and every new feature risks affecting existing logic.\u003c/p\u003e\n\u003cp\u003eA more reasonable approach is to define upfront:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003ePlugin\u003c/strong\u003e\n\u003cstrong\u003eExtension Point\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAnd let AI follow one principle:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eNew capabilities should preferably be added as new modules, not by modifying the core flow.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e","title":"Plugin Architecture and Extension Points: Guiding AI Toward Modular, Pluggable Development for Decoupled, Iterable Features"},{"content":"In web systems, there is a type of requirement that is especially prone to being implemented by AI as \u0026ldquo;it runs, but it\u0026rsquo;s dumb\u0026rdquo;:\nFrontend: Ask the server every 1 second \u0026#34;Is the task done?\u0026#34; Server: \u0026#34;No.\u0026#34; Ask again 1 second later: \u0026#34;Is it done?\u0026#34; This approach is called:\nPolling\nQuerying a status occasionally is fine, but for real-time scenarios such as AI streaming responses, OCR progress, document parsing, knowledge base construction, and background task status, high-frequency polling generates a large number of useless requests.\nA more appropriate solution is usually:\nSSE (Server-Sent Events)\nOr:\nWebSocket — a full-duplex, long-lived connection protocol\nThe core idea boils down to one sentence:\nDon\u0026rsquo;t make the client keep asking \u0026ldquo;Got any message yet?\u0026rdquo; — let the server push messages proactively when there is something new.\n📌 Technical Profile Long-lived Connection\nAfter a client and server establish a connection, it is not closed immediately but kept alive for a period of time to support subsequent data transfer.\nThere are two common solutions.\nSSE SSE (Server-Sent Events) is based on HTTP and is mainly used for:\nServer ↓ Client Suitable for:\nAI token streaming output; Background task progress; Log streams; Status notifications. WebSocket WebSocket supports:\nClient ↕ Server In other words:\nFull-Duplex Communication\nBetter suited for:\nOnline chat; Multi-user collaboration; Real-time games; Bidirectional real-time control. A quick rule of thumb:\nIf only the server needs to push to the client, prefer SSE; if both sides need to actively send messages at high frequency, consider WebSocket.\n💡 A Simple Analogy Polling is like constantly calling the repair shop:\nYou: Is it fixed? Clerk: No. 1 minute later: You: Is it fixed? Clerk: Not yet. While server push is more like:\nYou: Let me know when it\u0026#39;s fixed. ... Clerk: It\u0026#39;s fixed. So remember:\nPolling Client keeps asking SSE Server pushes whenever there\u0026#39;s a message WebSocket Both sides can speak anytime 1. The Most Common Mistake AI Makes: Simulating Real-Time with High-Frequency Polling Suppose you ask AI to implement:\nUpload a PDF and display processing progress in real time.\nIt easily generates:\nPOST /tasks GET /tasks/{task_id}/status And the frontend:\nsetInterval(async () =\u0026gt; { const result = await getTaskStatus(taskId) }, 1000) If 100 users query once per second, the server gets an extra:\n100 HTTP Requests per second. For a task that lasts 60 seconds:\n100 × 60 = 6000 requests But the actual number of state changes may only be a few dozen.\nTherefore:\nDon\u0026rsquo;t simulate real-time push with high-frequency polling.\n2. Architectural Principles 1. Decouple Task Execution from Streaming Not recommended:\nDocumentService ↓ Directly controls StreamingResponse ↓ Directly yields SSE A more reasonable approach:\nBusiness Task ↓ Progress Event ↓ Async Queue ↓ SSE Endpoint ↓ Browser In other words:\nThe task is responsible for producing events; the SSE endpoint is responsible for sending them.\nThis way, business logic doesn\u0026rsquo;t need to know the details of the HTTP protocol.\n2. Use a Unified Event Format Different tasks should not each define their own:\n{\u0026#34;percent\u0026#34;: 30} Or:\n{\u0026#34;status\u0026#34;: \u0026#34;almost_done\u0026#34;} Instead, unify on something like:\n{ \u0026#34;stage\u0026#34;: \u0026#34;embed\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Embedding chunks...\u0026#34;, \u0026#34;progress\u0026#34;: 72.5, \u0026#34;done\u0026#34;: false, \u0026#34;error\u0026#34;: false, \u0026#34;ts\u0026#34;: \u0026#34;2026-08-17T10:00:00\u0026#34; } Meaning of each field:\nstage Current stage message Status description progress Progress 0–100 done Whether completed error Whether failed ts Event timestamp This way the frontend can reuse the same progress component.\n3. The SSE Lifecycle Must Be Complete A reliable SSE implementation should consider at least four things:\nCreate Queue ↓ Continuously Send Events ↓ Keepalive ↓ Close and clean up after done / error Keepalive When there is no data for a long time, intermediate proxies may mistakenly assume the connection is dead.\nSo you can periodically send:\n: keepalive Explicit Termination Conditions Task completed:\ndone = true Task failed:\nerror = true Both should end the stream.\nClean Up Resources After the connection closes, the task queue should be removed; otherwise long-running operations may cause memory to accumulate.\nQueues Must Have an Upper Bound For example:\nasyncio.Queue(maxsize=200) This prevents events from piling up indefinitely when the client consumes too slowly.\nThis is related to the:\nBackpressure\nmechanism — when consumers can\u0026rsquo;t keep up with producers, the system needs a way to limit accumulation.\n4. Disable Caching and Proxy Buffering One common SSE problem:\nThe server keeps doing:\nyield event but the browser receives nothing for a long time, then suddenly gets a batch of events all at once.\nThis is usually caused by proxy buffering, so the common response configuration is:\nCache-Control: no-cache X-Accel-Buffering: no The purpose is simple:\nDeliver events to the client as soon as they are produced, instead of accumulating them and sending later.\n5. Separate Task Execution from Task Observation For long-running tasks such as OCR, document processing, and Embedding, a more reasonable structure is:\nClient ↓ Start Task ↓ task_id Background Task ↓ Continuously produce Progress Events Client ↓ SSE /tasks/{task_id}/progress That is:\nTask execution does the work; SSE observes the progress.\nThe two are linked via task_id.\n6. Why Not Overuse WebSocket WebSocket is more powerful, but it also means you need to handle extra concerns:\nHeartbeats; Reconnection; Authentication; Connection management; Multi-instance routing; Message ordering; Broadcasting; Cleanup on disconnect. If the business is just:\n20% 40% 80% 100% this kind of one-way progress push, SSE is usually simpler.\nSo:\nAn architecture isn\u0026rsquo;t more advanced because it uses more WebSockets — the protocol should match the communication pattern.\n3. What Do Long-Lived Connections and Streaming Push Bring? 1. Fewer Useless Requests Polling:\nClient → Server Client → Server Client → Server SSE:\nClient ───────── Server ↓ 20% ↓ 50% ↓ 100% Business events are pushed only when something actually changes.\n2. Lower Perceived Latency Perceived Latency\nEven if an AI response still takes 10 seconds in total:\nTraditional mode:\nWait 10 seconds ↓ Complete answer suddenly appears Streaming mode:\nStart seeing content at second 1 ↓ Continuous output ↓ Ends at second 10 Users will clearly feel the system is faster.\n3. A Natural Fit for AI Systems AI systems inherently involve:\nLLM Token Stream Document processing progress Embedding progress Web Search status Tool execution process Agent execution status So streaming push is usually a key foundational capability of Agent systems.\n4. Putting Prompts into Practice: Directly Constraining AI You can add the following rules to:\nProject Rules\n## Streaming and Long-lived Connection Rules 1. Do not use high-frequency polling to fetch real-time progress when server push is more appropriate. 2. Prefer SSE for server-to-client streaming: - AI token streaming - Background task progress - OCR/PDF processing progress - Logs - Notifications 3. Only use WebSocket when frequent bidirectional communication is required. 4. Separate task execution from streaming. Recommended architecture: Task → Progress Event → Async Queue → SSE Endpoint → Client 5. Use a unified event schema: { stage, message, progress, done, error, ts } 6. SSE must support keepalive. 7. Close the stream when done=true or error=true. 8. Clean up queues and resources after termination. 9. Event queues must have a finite capacity. 10. Disable response buffering for SSE where necessary. 11. Before generating polling code, check whether SSE or WebSocket is more appropriate. This is much better than saying:\n\u0026ldquo;Help me implement real-time progress.\u0026rdquo;\nbecause it prevents AI from reflexively generating setInterval().\n5. A Positive Example: The Real SSE Implementation in miniagent miniagent has already implemented a complete task-progress SSE architecture:\nTask ↓ ProgressTracker ↓ asyncio.Queue ↓ SSE Endpoint ↓ Browser 1. Each Task Has Its Own Event Queue miniagent\u0026rsquo;s ProgressTracker uses:\nclass ProgressTracker: _queues: dict[str, asyncio.Queue] = {} @classmethod def create(cls, task_id: str) -\u0026gt; asyncio.Queue: q: asyncio.Queue = asyncio.Queue(maxsize=200) cls._queues[task_id] = q return q Each task corresponds to its own Queue:\ntask_001 → Queue A task_002 → Queue B task_003 → Queue C And uses:\nmaxsize=200 to limit event backlog.\n2. The Business Layer Only Publishes Events miniagent publishes via:\nawait ProgressTracker.emit( task_id, stage=\u0026#34;embed\u0026#34;, message=\u0026#34;Embedding chunks...\u0026#34;, progress=72.5, ) emitting a unified structure:\n{ \u0026#34;stage\u0026#34;: stage, \u0026#34;message\u0026#34;: message, \u0026#34;progress\u0026#34;: round(progress, 1), \u0026#34;done\u0026#34;: done, \u0026#34;error\u0026#34;: error, \u0026#34;ts\u0026#34;: datetime.now().isoformat(), } The task itself doesn\u0026rsquo;t need to touch StreamingResponse, achieving decoupling between business logic and the SSE transport layer.\n3. The SSE Endpoint Continuously Consumes Events miniagent provides:\nGET /{task_id}/progress Core logic:\nqueue = ProgressTracker.get(task_id) event = await asyncio.wait_for( queue.get(), timeout=30.0, ) When new progress arrives, the Queue immediately hands the event to the SSE Endpoint.\n4. Send Keepalive After 30 Seconds Without Events miniagent on timeout:\nexcept asyncio.TimeoutError: yield \u0026#34;: keepalive\\n\\n\u0026#34; keeping the long-lived connection active even when the task has no new status for a while.\n5. Automatically Close and Clean Up on Completion or Failure Send the event:\nyield f\u0026#34;data: {json.dumps(event, ensure_ascii=False)}\\n\\n\u0026#34; Then check:\nif event.get(\u0026#34;done\u0026#34;) or event.get(\u0026#34;error\u0026#34;): break Finally:\nfinally: ProgressTracker.remove(task_id) In other words, the complete lifecycle is:\nCreate Queue ↓ Emit Progress ↓ SSE Push ↓ done / error ↓ Close Stream ↓ Remove Queue 6. Disable Caching and Proxy Buffering Finally, miniagent returns:\nreturn StreamingResponse( event_generator(), media_type=\u0026#34;text/event-stream\u0026#34;, headers={ \u0026#34;Cache-Control\u0026#34;: \u0026#34;no-cache\u0026#34;, \u0026#34;X-Accel-Buffering\u0026#34;: \u0026#34;no\u0026#34;, }, ) Where:\ntext/event-stream indicates the SSE data stream.\nAnd:\nCache-Control: no-cache X-Accel-Buffering: no are used to reduce the impact of caching and reverse-proxy buffering on real-time delivery.\n6. miniagent\u0026rsquo;s SSE Architecture flowchart LR A[\"Document / OCR / KB Task\"] --\u003e|\"emit()\"| B[\"ProgressTracker\"] B --\u003e C[\"asyncio.Queuemaxsize = 200\"] D[\"Browser\"] --\u003e|\"GET /tasks/{id}/progress\"| E[\"FastAPI SSE Endpoint\"] C --\u003e|\"await queue.get()\"| E E --\u003e|\"data: JSON\"| D E -. \"30s timeout\" .-\u003e F[\"keepalive\"] E --\u003e|\"done / error\"| G[\"Close Stream\"] G --\u003e H[\"Remove Queue\"] The most important boundary in this structure is:\nTask Producer ↓ Event Queue ↓ SSE Transport Layer ↓ Client rather than letting task code directly control the HTTP Response.\nBelow is a more detailed data flow diagram of the SSE implementation:\nflowchart TD U[\"Browser / Frontend\"] --\u003e|\"1. Start Task\"| API[\"FastAPI Task API\"] API --\u003e|\"2. return task_id\"| U API --\u003e|\"3. create task\"| TASK[\"Document / OCR / KB Task\"] TASK --\u003e|\"4. emit()\"| PT[\"ProgressTracker\"] PT --\u003e Q[\"asyncio.Queuetask_id → Queuemaxsize=200\"] U --\u003e|\"5. GET /{task_id}/progress\"| SSE[\"FastAPI SSE Endpoint\"] Q --\u003e|\"6. await queue.get()\"| SSE SSE --\u003e|\"7. data: JSON\"| U SSE -. \"30s timeout\" .-\u003e KA[\"Keepalive\"] KA -.-\u003e U TASK --\u003e|\"progress / stage / message\"| PT TASK --\u003e|\"done / error\"| PT SSE --\u003e|\"8. done / error\"| CLOSE[\"Close Stream\"] CLOSE --\u003e CLEAN[\"9. Remove Queue\"] 7. A Checklist for AI Next time you ask AI to implement a real-time feature, you can require it to check:\nStreaming Architecture Checklist □ Is polling really needed here? □ If it\u0026#39;s only Server → Client, should SSE be preferred? □ Is bidirectional WebSocket really necessary? □ Is task execution decoupled from Streaming? □ Is a unified event format used? □ Does the Queue have a capacity limit? □ Does the SSE have keepalive? □ Does it end after done / error? □ Are resources cleaned up after ending? □ Are caching and proxy buffering disabled? Summary AI easily generates:\nsetInterval ↓ GET /status ↓ Not done ↓ Request again For real-time progress, AI output, and long-running tasks, this often produces a flood of useless requests.\nA more reasonable design is:\nBackground Task ↓ Progress Event ↓ Async Queue ↓ SSE ↓ Browser Only when frequent bidirectional communication is genuinely required should you use:\nClient ↕ WebSocket ↕ Server miniagent already forms a complete SSE task-progress push chain through ProgressTracker, a bounded asyncio.Queue, FastAPI\u0026rsquo;s StreamingResponse, 30-second Keepalive, done/error termination, and Queue cleanup.\nFor AI programming, what we need to avoid is not just incorrect code, but also this kind of architectural waste:\nWhen the server could proactively tell you the result, the client still keeps asking: \u0026ldquo;Is it done yet?\u0026rdquo;\nThis is precisely the value of long-lived connections and streaming push.\nOpen Source Code github gitee 🪐 Wishing you good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0116/","summary":"\u003cp\u003eIn web systems, there is a type of requirement that is especially prone to being implemented by AI as \u0026ldquo;it runs, but it\u0026rsquo;s dumb\u0026rdquo;:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eFrontend:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eAsk the server every 1 second\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026#34;Is the task done?\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eServer:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026#34;No.\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eAsk again 1 second later:\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u0026#34;Is it done?\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis approach is called:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003ePolling\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eQuerying a status occasionally is fine, but for real-time scenarios such as AI streaming responses, OCR progress, document parsing, knowledge base construction, and background task status, high-frequency polling generates a large number of useless requests.\u003c/p\u003e\n\u003cp\u003eA more appropriate solution is usually:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSSE (Server-Sent Events)\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eOr:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eWebSocket — a full-duplex, long-lived connection protocol\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe core idea boils down to one sentence:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eDon\u0026rsquo;t make the client keep asking \u0026ldquo;Got any message yet?\u0026rdquo; — let the server push messages proactively when there is something new.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e","title":"Long-Lived Connections and Streaming Push: Standardizing SSE / WebSocket Implementations to Replace Inefficient Polling"},{"content":"There is a very common problem with AI-generated code:\nThe code runs, but slows down as soon as multiple users access it.\nEspecially in Python Web projects, even when using FastAPI and functions are written as async def, operations such as database queries, HTTP requests, and large language model (LLM) calls may still block synchronously.\nThe key issue is:\nasync def is merely the entry point of an asynchronous function — it does not automatically turn the synchronous code inside it into asynchronous code.\nFor systems that heavily rely on external I/O — such as web services, knowledge bases, and agent platforms — establishing a unified async and concurrency architecture is an important foundation for improving system throughput.\n📌 Technical Profile Asynchronous Programming\nWhen a program is waiting for external operations — such as databases, networks, or LLMs — to return, it does not hold onto the execution thread idly. Instead, it yields the execution opportunity to other tasks.\nClosely related is:\nConcurrency\nAdvancing multiple tasks within the same time period, rather than strictly completing one before starting the next.\nNote the distinction:\nConcurrency — multiple tasks progress in an interleaved fashion; Parallelism — multiple tasks truly execute simultaneously. Web backends typically focus on concurrency first.\n💡 A Simple Analogy Imagine the server as a restaurant waiter.\nIn synchronous mode, after the waiter sends Customer A\u0026rsquo;s order to the kitchen, they stand there waiting for the food to be ready:\nA places order ↓ Waiting for kitchen... ↓ Food is ready ↓ Then serve B In asynchronous mode:\nA places order → Kitchen processes ↓ Waiter → Serves B → Serves C → Serves D ↓ A\u0026#39;s food is ready → Come back to handle A The number of waiters hasn\u0026rsquo;t increased — the only difference is:\nThey no longer just stand around while waiting.\nThis is the core idea behind asynchronous programming.\n1. The Most Common Mistake AI Makes: Asynchronous on the Surface, Blocking in Reality For example, AI easily generates:\nasync def handle_request(): result = requests.get(url) return result.text It appears to use:\nasync def But requests.get() is still a synchronous blocking call.\nA similar problem is:\ntime.sleep(5) appearing inside an asynchronous function.\nYou should use async-compatible implementations instead, such as:\nasync with httpx.AsyncClient() as client: response = await client.get(url) And:\nawait asyncio.sleep(5) So to judge whether code is truly asynchronous, you can\u0026rsquo;t just look at whether there is an async def — you also need to check:\nWhether any blocking operations have slipped into the entire call chain.\n2. Architectural Principles 1. Async Must Run Through the Entire Call Chain A truly reliable asynchronous architecture should look like:\nflowchart TD A[FastAPI API] --\u003e B[Async Service] B --\u003e C[Async Repository] C --\u003e D[AsyncSession] D --\u003e E[Async Database Driver] Layers involving I/O should stay asynchronous as much as possible.\nSo-called I/O (Input/Output) operations mainly include:\nDatabase access; HTTP requests; LLM calls; Redis access; File read/write; Web Search; Vector database access. The biggest cost of these operations is often not CPU computation, but waiting for external results.\nTherefore, the principle is simple:\nI/O-intensive operations should prefer asynchronous interfaces.\n2. Tasks That Can Run Concurrently Should Not Wait Sequentially Asynchronous does not automatically mean concurrent.\nFor example:\nuser = await get_user() kb = await search_kb() web = await search_web() Although all three functions are asynchronous, they still execute sequentially.\nIf the three tasks are independent, you can do:\nuser, kb, web = await asyncio.gather( get_user(), search_kb(), search_web(), ) Suppose the three tasks each take:\nUser info 0.4s KB search 1.2s Web Search 1.5s Sequential execution takes approximately:\n0.4 + 1.2 + 1.5 = 3.1s Concurrent execution is closer to the slowest task:\n≈ 1.5s But concurrency should not be overused either.\nFor example:\nCreate order ↓ Deduct inventory ↓ Generate payment record Each step depends on the previous one, so you can\u0026rsquo;t simply put them into asyncio.gather().\nSo remember this rule:\nTasks without dependencies are candidates for concurrency; tasks with dependencies should remain sequential.\n3. CPU-Intensive Tasks Must Not Block the Event Loop Asynchrony primarily solves the problem of I/O waiting.\nFor tasks such as:\nOCR; Image processing; Video transcoding; PDF re-computation; Large-scale data computation; Local model inference; These typically fall under:\nCPU-bound Task\nThey are not \u0026ldquo;waiting for someone else\u0026rdquo; — they genuinely and continuously occupy the CPU.\nIf you write:\nasync def endpoint(): result = heavy_cpu_task() Even though the function is async, it may still block:\nEvent Loop\nTherefore, heavy computation should typically be moved to:\nThread Pool Process Pool Background Worker In short:\nI/O-intensive ↓ async / await CPU-intensive ↓ Thread / Process / Worker 3. Async Resources Should Be Managed Centrally Resources such as database Engines, SessionFactories, and HTTP Clients should not be repeatedly created inside every business function.\nA more reasonable architecture is:\nApplication Startup ↓ ServiceContainer ↓ Async Engine / Client ↓ Repository ↓ Service Centrally managing these resources reduces redundant connection creation and makes initialization, shutdown, and exception handling much clearer.\n3. What Does an Async Architecture Actually Bring? The most important thing is not making a single database query suddenly faster.\nSuppose the database query itself takes 500ms:\nSynchronous: 500ms Asynchronous: likely still 500ms The difference is whether the server can continue processing other requests while waiting those 500ms.\nSynchronous:\nRequest A ████████████ Request B ████████████ Asynchronous:\nRequest A ████────████ Request B ███────████ Request C ███────████ Therefore, what an async architecture primarily improves is:\nThroughput — the number of requests a system can handle per unit of time.\nThis is especially important for AI systems, because a single Agent request may involve:\nDatabase ↓ LLM ↓ Knowledge Base ↓ Embedding ↓ Reranker ↓ Web Search ↓ Tool Calls ↓ LLM Called Again A significant amount of time is spent waiting on networks and external services.\nThis is why AI applications are naturally well-suited for asynchronous architectures.\n4. Putting Prompts into Practice: Directly Constraining AI Rather than manually checking every time, it\u0026rsquo;s better to write the async rules into the Project Rules:\n## Async and Concurrency Rules This project adopts an async-first architecture for I/O-intensive operations. 1. FastAPI routes that perform I/O operations should use async def. 2. Database access must use SQLAlchemy AsyncSession. 3. Repository database methods must be asynchronous. 4. Never directly call blocking I/O inside async functions. Avoid using: - requests - time.sleep - synchronous database sessions Recommended: - httpx.AsyncClient - asyncio.sleep - AsyncSession 5. Independent I/O tasks may use asyncio.gather. 6. Do not parallelize operations that have data dependencies or transactional ordering requirements, and do not block on CPU-intensive operations. 7. CPU-intensive operations must not block the event loop. Use thread pools, process pools, or background workers where appropriate. 8. Maintain the async call chain: API → Service → Repository → Async Driver. 9. Before generating code, check whether all called libraries are synchronous and whether they may block the event loop. This is far more explicit than simply telling the AI:\n\u0026ldquo;Help me optimize performance.\u0026rdquo;\n5. A Positive Example: The Async Architecture of miniagent miniagent is an agent platform built on FastAPI, SQLAlchemy, knowledge base retrieval, and an Agent Runtime.\nIts database access is not just a few async def at the API layer — it forms a complete asynchronous data access chain.\n1. Unified Creation of the Async Engine In miniagent\u0026rsquo;s ServiceContainer:\nfrom sqlalchemy.ext.asyncio import ( create_async_engine, async_sessionmaker, AsyncSession, ) database_url = f\u0026#34;sqlite+aiosqlite:///{db_path}\u0026#34; self.engine = create_async_engine( database_url, echo=False, future=True, ) self.session_factory = async_sessionmaker( bind=self.engine, class_=AsyncSession, autoflush=False, autocommit=False, expire_on_commit=False, ) What this forms is:\nFastAPI ↓ SQLAlchemy Async Engine ↓ AsyncSession ↓ aiosqlite ↓ SQLite Rather than \u0026ldquo;FastAPI is async, but the underlying database is still accessed synchronously.\u0026rdquo;\n2. Repository Unified Asynchronization miniagent\u0026rsquo;s data access layer includes:\nasync_agent.py async_chat.py async_chunk.py async_document.py async_embedding.py async_knowledge_base.py async_llm.py ... In other words, asynchrony is adopted as the unified design approach for the Repository (data access layer), rather than a localized optimization for a few interfaces.\n3. Session Lifecycle Is Also Asynchronous miniagent defines a unified AsyncBaseDatabase:\n@asynccontextmanager async def get_session(self): async with self.AsyncSessionLocal() as session: try: yield session await session.commit() except SQLAlchemyError: await session.rollback() raise finally: await session.close() The database Session\u0026rsquo;s:\nCreation Commit Rollback Close are all uniformly managed within an asynchronous lifecycle.\n4. Database Operations Genuinely Use await For example, miniagent\u0026rsquo;s chat data access:\nasync def get_user_session( self, session_id: int, user_id: int, ): async with self.get_session() as session: stmt = ( select(ChatSession) .where( ChatSession.id == session_id, ChatSession.user_id == user_id, ) ) return ( await session.execute(stmt) ).scalar_one_or_none() What truly matters is not the:\nasync def in front of the function, but the database operation itself:\nawait session.execute(stmt) This is what gives the event loop the opportunity to continue processing other tasks while waiting for the database.\n5. The Complete Async Chain The main asynchronous chain of miniagent is shown below:\nflowchart TD A[HTTP Request] --\u003e B[FastAPI] B --\u003e C[Async API] C --\u003e D[Service] D --\u003e E[Async Repository] E --\u003e F[AsyncBaseDatabase] F --\u003e G[SQLAlchemy AsyncSession] G --\u003e H[aiosqlite] H --\u003e I[SQLite] G -. Waiting .-\u003e J[Event Loop] J --\u003e K[Other Requests] When a request is waiting for the database, execution control can return to the Event Loop, and the server continues advancing other requests.\nThis is the core of how an async architecture improves system throughput.\nThe diagram below shows more details of the async chain:\nflowchart TD U[\"User / Client\"] --\u003e API[\"FastAPIAsync API\"] API --\u003e EL[\"Event Loop\"] EL --\u003e SVC[\"Service LayerAsync Service Layer\"] SVC --\u003e AGENT[\"Agent Runtime\"] SVC --\u003e REPO[\"Async Repository\"] SVC --\u003e KB[\"Knowledge Base\"] SVC --\u003e WEB[\"Web Search / HTTP\"] SVC --\u003e LLM[\"LLM\"] REPO --\u003e BASE[\"AsyncBaseDatabase\"] BASE --\u003e SESSION[\"SQLAlchemy AsyncSession\"] SESSION --\u003e DRIVER[\"aiosqlite\"] DRIVER --\u003e DB[(\"SQLite\")] AGENT -. \"await\" .-\u003e EL SESSION -. \"await\" .-\u003e EL KB -. \"await\" .-\u003e EL WEB -. \"await\" .-\u003e EL LLM -. \"await\" .-\u003e EL EL --\u003e OTHER[\"Other RequestsContinue Processing Other Requests\"] SVC --\u003e CPU[\"CPU-heavy TasksOCR / PDF / Heavy Compute\"] CPU --\u003e WORKER[\"Thread / Process / WorkerMoved Off the Event Loop\"] 6. A Simple Checklist for AI After having AI complete backend code, you can ask it to self-check:\nAsync Architecture Checklist □ Are I/O operations using async APIs? □ Is the database using AsyncSession? □ Are there synchronous blocking libraries called inside async functions? □ Can independent I/O tasks run concurrently? □ Are tasks with dependencies incorrectly parallelized? □ Do CPU-heavy tasks block the Event Loop? □ Is the following chain maintained: API Service Repository Async Driver Complete async chain? Summary AI easily produces code like:\nCall A ↓ Wait ↓ Call B ↓ Wait ↓ Call C This kind of code is fine for a demo, but once it enters a real concurrent environment, it easily becomes a system bottleneck.\nTherefore, a few clear rules should be established upfront:\nI/O defaults to async ↓ Async runs through the call chain ↓ Independent tasks consider concurrency ↓ CPU-heavy tasks moved off the Event Loop ↓ Async resources managed centrally Projects like miniagent bake async capability into the infrastructure through AsyncSession, async Repositories, unified Session management, and the FastAPI async call chain — rather than scattering it across a few interfaces.\nFor AI programming, what we ultimately need to constrain is not just:\n\u0026ldquo;Can this code run?\u0026rdquo;\nWe also need to ask:\n\u0026ldquo;Will it block the entire server while it waits?\u0026rdquo;\nThis is precisely the value of an async and concurrency architecture.\nOpen Source Code github gitee 🪐 Wishing you good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0115/","summary":"\u003cp\u003eThere is a very common problem with AI-generated code:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eThe code runs, but slows down as soon as multiple users access it.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eEspecially in Python Web projects, even when using FastAPI and functions are written as \u003ccode\u003easync def\u003c/code\u003e, operations such as database queries, HTTP requests, and large language model (LLM) calls may still block synchronously.\u003c/p\u003e\n\u003cp\u003eThe key issue is:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003easync def\u003c/code\u003e is merely the entry point of an asynchronous function — it does not automatically turn the synchronous code inside it into asynchronous code.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eFor systems that heavily rely on external I/O — such as web services, knowledge bases, and agent platforms — establishing a unified async and concurrency architecture is an important foundation for improving system throughput.\u003c/p\u003e\n\u003chr\u003e","title":"Async and Concurrency Architecture: Fixing AI's Synchronous Blocking Patterns to Improve System Throughput"},{"content":"AI (Artificial Intelligence) has a very common problem when writing code:\nIt loves to \u0026ldquo;hardcode things on a whim.\u0026rdquo;\ntimeout = 30 max_retries = 3 model = \u0026#34;qwen3:14b\u0026#34; raise ValueError(\u0026#34;User does not exist\u0026#34;) Individually, there\u0026rsquo;s nothing fundamentally wrong with any of these lines.\nBut when dozens of modules each have their own 30, 3, model names, and prompt text, maintainers start getting headaches:\nWhat do these values mean? Where should they be changed? What happens when switching environments? How do we support English?\nTherefore, AI programming projects should establish a clear architectural rule upfront:\nValues with business significance, environment-specific differences, or user-visible meaning are not allowed to be scattered across business code.\nThis problem is primarily addressed through two mechanisms:\nCentralized Configuration + I18n (Internationalization).\n📌 Technical Profile Centralized Configuration Centralize parameters that may vary by environment, business rules, or runtime strategy into a single configuration entry point.\nFor example:\nService addresses and ports Timeout durations, retry counts Maximum concurrency Password security policies Model names Feature flags Business code is only responsible for reading configuration, not for deciding these values.\nI18n (Internationalization) There are 18 letters between the first and last letters of \u0026ldquo;Internationalization,\u0026rdquo; hence the abbreviation I18n.\nIts core idea is:\nText that users can see should not be written directly into business code, but should be read from a unified language resource.\nFor example:\nt(\u0026#34;auth.login_failed\u0026#34;) In Chinese, this can return:\n用户名或密码错误 In English, it can return:\nIncorrect username or password 💡 In Plain Terms: Don\u0026rsquo;t Let Every Room Decide Its Own Thermostat Think of a software project as a hotel.\nA hotel doesn\u0026rsquo;t let the renovation worker in each room decide:\n\u0026ldquo;I think 26°C feels comfortable, let me weld it into the wall.\u0026rdquo;\nInstead, adjustable parameters like temperature are connected to a unified control system — this is centralized configuration.\nSimilarly, a hotel doesn\u0026rsquo;t weld Chinese prompts into every business process; it prepares unified multilingual scripts.\nThis is I18n.\nAI programming especially needs this kind of constraint. Because AI excels at quickly completing localized tasks, without architectural rules, it can easily write in different files:\ntimeout = 30 for _ in range(3): ... raise Exception(\u0026#34;Login failed\u0026#34;) Each snippet works, but the project gradually loses unified rules.\n1. What Should Be Forbidden? Magic Values typically refer to numbers or strings that appear directly in code but whose business meaning is not apparent.\nFor example:\nif failed_count \u0026gt;= 5: lock_user(10) What is 5? Is 10 in seconds, minutes, or hours?\nA clearer way to write this:\nif failed_count \u0026gt;= settings.login_max_failed_attempts: lock_user(settings.login_lock_duration_minutes) Hard Coding covers a broader scope, for example:\nAPI_URL = \u0026#34;http://127.0.0.1:8088\u0026#34; model = \u0026#34;qwen3:14b\u0026#34; message = \u0026#34;User does not exist\u0026#34; These values should be configurable, replaceable, or translatable, yet they get stuffed directly into program logic.\nBut note:\nForbidding magic values does not mean forbidding all literals.\nFor example:\nif count \u0026gt; 0: The meaning of 0 here is already perfectly clear; there\u0026rsquo;s no need to create a ZERO = 0 just for the sake of \u0026ldquo;zero hardcoding.\u0026rdquo;\nWhat should be strictly managed is:\nData Where It Should Go Environment, deployment, runtime parameters Centralized configuration API Keys, passwords, secrets Environment variables or secret management systems Stable domain states Constants or enumerations User-visible text I18n Literals with no business meaning and clear semantics Can remain as-is API stands for Application Programming Interface; Secret here refers to sensitive information such as keys.\n2. Architectural Standards: Give Every Type of Value a Home In practice, the rules can be condensed into four principles.\n1. Variable Parameters → Configuration Don\u0026rsquo;t:\ntimeout = 30 max_tokens = 4000 Instead:\nsettings.request_timeout_seconds settings.max_conversation_tokens Time and capacity configurations should ideally include the unit directly in the name, for example:\nlogin_lock_duration_minutes request_timeout_seconds max_file_size_mb 2. Sensitive Information → Environment Variables Forbidden:\nAPI_KEY = \u0026#34;sk-xxxxxxxx\u0026#34; JWT_SECRET = \u0026#34;123456\u0026#34; API Keys, passwords, Tokens (authentication tokens), Secrets, and other sensitive information should not enter source code.\nJWT stands for JSON Web Token, \u0026ldquo;a commonly used identity authentication token.\u0026rdquo;\n3. Stable Domain Values → Constants or Enumerations Don\u0026rsquo;t copy everywhere:\nif role == \u0026#34;admin\u0026#34;: Use an Enumeration:\nclass UserRole(str, Enum): ADMIN = \u0026#34;admin\u0026#34; USER = \u0026#34;user\u0026#34; Then:\nif role == UserRole.ADMIN: 4. User-Facing Text → I18n Don\u0026rsquo;t:\nraise ValueError(\u0026#34;User account does not exist\u0026#34;) Instead:\nraise ValueError(t(\u0026#34;user.not_found\u0026#34;)) Language resource:\nuser: not_found: User account does not exist Sentences with variables should also not be assembled via string concatenation:\nauth: account_locked: \u0026#34;Account is locked. Please try again in {minutes} minutes.\u0026#34; Call:\nt(\u0026#34;auth.account_locked\u0026#34;, minutes=10) This way, English can use a completely different word order without needing to modify business logic.\n3. Why Does AI Programming Especially Need These Rules? Traditional developers might remember:\n\u0026ldquo;This 30 is the timeout duration.\u0026rdquo;\nAI doesn\u0026rsquo;t naturally possess this kind of long-term project memory.\nEven if the project already has:\nsettings.max_tool_calls Without an explicit requirement to search existing configuration first, AI might still generate:\nfor _ in range(5): The functionality is correct, but the architecture starts to diverge.\nSo AI programming shouldn\u0026rsquo;t only check:\nCan the code run?\nIt should also check:\nWho should manage this value?\nAfter establishing centralized configuration and I18n, several direct benefits follow: modifying parameters only requires changing the unified entry point; switching between development, testing, and production environments becomes easier; adding a language doesn\u0026rsquo;t require searching the entire project; and Code Review can quickly identify bare numbers, hardcoded addresses, and user-facing text.\nMore importantly:\nArchitectural rules reduce AI\u0026rsquo;s freedom but increase the entire project\u0026rsquo;s consistency.\n4. Write the Rules Directly into AI Prompts Prompts shouldn\u0026rsquo;t just say:\n\u0026ldquo;Please write high-quality, maintainable code.\u0026rdquo;\nThat\u0026rsquo;s too abstract.\nYou can directly include project-level rules:\n## Configuration \u0026amp; I18n Rules The following must be observed when generating or modifying code: 1. Adding new magic values and hardcoded parameters with business significance, environment-specific differences, or user-visible meaning is forbidden. 2. Variable parameters such as timeouts, retries, model names, API addresses, ports, paths, capacity limits, and feature flags must be incorporated into the existing centralized configuration system. 3. Before adding new configuration, search existing configuration first. When a semantically identical configuration already exists, it must be reused; duplicate definitions are forbidden. 4. Sensitive information such as API Keys, passwords, Tokens, and Secrets must not be written into source code. 5. Stable domain states and values should use constants or enumerations. 6. All user-visible text must be incorporated into the existing I18n system. Hardcoding Chinese or English prompts directly in business code is forbidden. 7. When adding new I18n content: - Use keys that express business semantics; - Complete all languages supported by the project; - Use placeholders for dynamic content; - Do not concatenate multilingual sentences. 8. Time, size, and capacity configurations should include explicit units, e.g.: timeout_seconds lock_duration_minutes max_file_size_mb 9. After completion, check: - Were any new business magic values added? - Were environment parameters hardcoded? - Is there any user-visible hardcoded text? - Were existing configurations or I18n keys duplicated? If issues are found, refactor first before outputting the final code. The most important sentence here is actually:\nBefore adding something new, search for existing implementations first.\nOtherwise, even if AI knows it \u0026ldquo;should be configured,\u0026rdquo; it might create a second configuration system.\n5. Positive Outcome: Real Implementation in miniagent miniagent is an agent platform whose backend has already placed configuration and I18n into the infrastructure layer, with the project supporting both Chinese and English.\nCentralized Configuration In miniagent\u0026rsquo;s backend/app/core/config.py, configuration is uniformly defined using Pydantic Settings (a Pydantic configuration management component):\nclass Settings(BaseSettings): api_port: int = Field( default=8088, description=\u0026#34;API port\u0026#34; ) max_concurrent_requests: int = Field( default=10, description=\u0026#34;Maximum concurrency\u0026#34; ) max_conversation_tokens: int = Field( default=4000, description=\u0026#34;Maximum number of tokens in a single conversation\u0026#34; ) max_tool_calls: int = Field( default=5, description=\u0026#34;Maximum number of tool calls\u0026#34; ) Along with the configuration:\nmodel_config = SettingsConfigDict( env_file=\u0026#34;.env\u0026#34;, env_file_encoding=\u0026#34;utf-8\u0026#34;, case_sensitive=False, extra=\u0026#34;ignore\u0026#34; ) This way, the .env environment configuration file can override default values, and business modules only need to consume the unified settings.\nSecurity rules work the same way:\npassword_min_length: int = Field(default=8, ge=1) login_max_failed_attempts: int = Field( default=5, ge=1 ) login_lock_duration_minutes: int = Field( default=10, ge=1 ) Especially login_lock_duration_minutes — the name directly includes the unit \u0026ldquo;minutes,\u0026rdquo; eliminating the ambiguity of what 10 represents.\nI18n Infrastructure miniagent\u0026rsquo;s backend/app/core/i18n/i18n.py reads the system language and loads the corresponding YAML (YAML Ain\u0026rsquo;t Markup Language, a commonly used configuration data format) language file:\nself._language = ( await self._setting_service.get_system_language() ) locale_file = Path( f\u0026#34;app/locales/{self._language}.yaml\u0026#34; ) if locale_file.exists(): with open(locale_file, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: translations = yaml.safe_load(f) or {} Then through the unified:\nt(\u0026#34;auth.login_failed\u0026#34;) It retrieves user-facing text, rather than letting each business module decide whether to use Chinese or English.\nminiagent\u0026rsquo;s current backend language resource directory contains:\nbackend/app/locales/ ├── zh.yaml └── en.yaml The actual Chinese resources already show:\ncommon: success: 操作成功 failed: 操作失败 auth: login_failed: 用户名或密码错误 unauthorized: 未授权，请先登录 token_invalid: Token 无效或已过期 account_locked: \u0026gt; 账户因连续登录失败已锁定， 请在 {minutes} 分钟后重试， 或联系管理员解锁。 Business code is responsible for providing data like minutes, while language resources are responsible for determining the final expression.\nTherefore, miniagent\u0026rsquo;s overall approach can be distilled into two pathways:\nEnvironment Variables ↓ Settings ↓ Business Code System Language ↓ I18n ↓ zh.yaml / en.yaml ↓ t(\u0026#34;xxx.xxx\u0026#34;) ↓ Business Code Both mechanisms address the same core problem:\nSeparate data that is prone to change from business logic that is relatively stable.\n6. Conclusion: Code Is Responsible for Doing Things, Not for \u0026ldquo;Casually Deciding Rules\u0026rdquo; The real danger of AI programming isn\u0026rsquo;t necessarily that it writes incorrect code.\nThe more common scenario is the opposite:\nEvery small snippet of code works, but the entire project becomes increasingly difficult to maintain.\ntimeout = 30 Might not be wrong.\nmax_retries = 3 Also might not be wrong.\nraise ValueError(\u0026#34;User does not exist\u0026#34;) Might even fully meet the current requirements.\nWhat should really be asked is:\nWhy should this value appear here?\nGood architecture defines the rules upfront:\nRuntime Parameters → Configuration User-Facing Text → I18n Domain States → Constants / Enumerations Sensitive Info → Environment Variables Business Code → Consumes the Above Definitions Like miniagent, establishing Settings, .env, constants, and I18n infrastructure in advance is essentially drawing construction boundaries for AI.\nIt\u0026rsquo;s not about restricting AI from writing code, but about restricting AI from casually creating new rules.\nUltimately, what we want isn\u0026rsquo;t \u0026ldquo;AI writes faster,\u0026rdquo; but:\nThe faster AI writes, the more organized the project remains.\nOpen Source Code github gitee 🪐 Best of luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0114/","summary":"\u003cp\u003eAI (Artificial Intelligence) has a very common problem when writing code:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eIt loves to \u0026ldquo;hardcode things on a whim.\u0026rdquo;\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003etimeout\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e30\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emax_retries\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e3\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emodel\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;qwen3:14b\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"ne\"\u003eValueError\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;User does not exist\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIndividually, there\u0026rsquo;s nothing fundamentally wrong with any of these lines.\u003c/p\u003e\n\u003cp\u003eBut when dozens of modules each have their own \u003ccode\u003e30\u003c/code\u003e, \u003ccode\u003e3\u003c/code\u003e, model names, and prompt text, maintainers start getting headaches:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWhat do these values mean? Where should they be changed? What happens when switching environments? How do we support English?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eTherefore, AI programming projects should establish a clear architectural rule upfront:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eValues with business significance, environment-specific differences, or user-visible meaning are not allowed to be scattered across business code.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis problem is primarily addressed through two mechanisms:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCentralized Configuration + I18n (Internationalization).\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e","title":"Centralized Configuration and I18n: Strictly Forbid AI Magic Values and Hardcoded Parameters"},{"content":"Many projects start with just one database. So AI naturally writes:\nasync def get_user(user_id: int): async with sqlite_session() as session: ... Or even more directly:\nconn = sqlite3.connect(\u0026#34;app.db\u0026#34;) The feature certainly works, but problems will show up later.\nToday it\u0026rsquo;s SQLite, tomorrow it might need to switch to PostgreSQL \u0026hellip;\nAs the codebase grows, the project quickly becomes:\nBusiness code tightly coupled to a specific storage technology.\nThis is exactly the problem that Multi-Data-Source Architecture aims to solve.\n📌 Technology Card Multi-Data-Source Architecture\nA system that simultaneously uses multiple different data sources and manages them through unified architectural boundaries, instead of letting business code depend directly on specific database implementations.\nCommon data sources include:\nSQLite PostgreSQL MySQL DuckDB The most important thing here is not:\n\u0026ldquo;How many databases are supported.\u0026rdquo;\nBut rather:\nWhether business code needs to know what the underlying database actually is.\n💡 One-Sentence Summary You can understand multi-data-source architecture as:\nThe business layer is only responsible for \u0026ldquo;what data I need\u0026rdquo;; the infrastructure layer handles \u0026ldquo;where to get it and how\u0026rdquo;.\nAn Intuitive Analogy: Databases Are Like Shipping Companies Suppose you run an e-commerce business. The business team only cares about:\nGetting products into customers\u0026#39; hands. As for the shipping method:\nSF Express JD Logistics DHL Sea freight Air freight None of these should be written into the order business itself. A wrong design might look like:\nOrder is domestic: call SF Express API Order is for Europe: call DHL API Order is oversized: call sea freight API The more reasonable approach is for the order system to simply say:\n\u0026ldquo;Ship this for me.\u0026rdquo;\nAnd let the logistics layer decide how to actually do it.\nDatabases are the same. The business layer should say:\nQuery users Save an Agent Read the knowledge base Run analytical SQL Rather than:\nUse this syntax for SQLite Use that syntax for PostgreSQL Handle DuckDB as a special case I. Cross-Data-Source Architecture 1. The Business Layer Must Not Depend on Databases Directly A healthy call chain should look like:\nRouter ↓ Service ↓ Repository / Data Access ↓ Database Driver / ORM ↓ Database Where:\nRepository (the repository layer)\nis responsible for encapsulating data access.\nA business Service should not know about:\nSQLAlchemy sqlite3 aiosqlite psycopg DuckDB connection It should only know:\nuser = await user_repository.get_by_id(user_id) Not:\nasync with session.execute(...): 2. The Point of an ORM Is Not Just \u0026ldquo;Writing Less SQL\u0026rdquo; This brings up an important term:\nORM / Object-Relational Mapping\nAn ORM establishes a mapping between Python Objects and Relational Database Tables; for example, User maps to the users table.\nWhen many people think about ORMs, they only think of:\nWriting less SQL.\nBut in a multi-database architecture, it has an even more important value:\nIsolating the huge amount of syntax and driver differences between databases.\nFor example, SQLAlchemy can adapt to different databases through different:\nDialects\nThe application layer writes:\nselect(User).where(User.id == user_id) and under the hood this is translated into the appropriate SQL depending on the database.\n3. But an ORM Is Not a \u0026ldquo;Universal Compatibility Layer\u0026rdquo; Either It must be made clear that significant differences still exist between databases, for example:\nData types JSON support Full-text search Pagination syntax UPSERT Auto-increment primary keys Date functions Locking mechanisms Transaction isolation Indexes Window functions If AI writes lots of:\ntext(\u0026#34;some database-specific SQL\u0026#34;) then even though an ORM is being used:\nThe project is still locked to one database.\nTherefore the rule should be:\nPrefer the ORM\u0026rsquo;s generic expression capabilities; database-specific syntax may only live in the infrastructure layer.\nII. The Main Business Database of miniagent 1. Creation miniagent\u0026rsquo;s current main business database uses:\nSQLite + SQLAlchemy Async At application startup, everything is created centrally in the ServiceContainer:\ndatabase_url = f\u0026#34;sqlite+aiosqlite:///{db_path}\u0026#34; self.engine = create_async_engine( database_url, echo=False, future=True, ) self.session_factory = async_sessionmaker( bind=self.engine, ... ) Then these are uniformly injected into different data access objects:\nengine session_factory self.user_db = AsyncUserDatabase( self.engine, self.session_factory ) self.chat_db = AsyncChatDatabase( self.engine, self.session_factory ) self.kb_db = AsyncKnowledgeBaseDatabase( self.engine, self.session_factory ) This structure is critically important.\nBecause business Services do not each call create_engine() themselves; they all reuse the shared database infrastructure.\n2. Unified Session Management, So AI Doesn\u0026rsquo;t Scatter commit/rollback Everywhere There is another aspect of database access that AI easily messes up:\nTransactions\nAI tends to write in one place:\nawait session.commit() In another place:\nawait session.rollback() And forget to close in yet another.\nSo the Session lifecycle ends up scattered everywhere.\nminiagent provides a unified AsyncBaseDatabase, which contains:\n@asynccontextmanager async def get_session(self): async with self.AsyncSessionLocal() as session: try: yield session await session.commit() except SQLAlchemyError: await session.rollback() raise finally: await session.close() Here, a:\nTransaction\ncan be understood as:\nA group of database operations that either all succeed or all fail.\nThis way the Repository layer can uniformly use:\nasync with self.get_session() as session: ... instead of making every business function figure out again:\nWhen to commit? Should we roll back on failure? When to close? 3. Multiple Data Sources Does Not Mean \u0026ldquo;Forcibly Unifying All Databases\u0026rdquo; This is one of the most important points in this article.\nminiagent itself is a great example.\nIts:\nUsers Agents Knowledge base configuration Permissions Chat history System settings are typical business data.\nWell suited for:\nSQLAlchemy ↓ SQLite But the SQL Agent serves a different scenario:\nAnalytical data queries.\nSo miniagent also has a separate DuckDBManager:\nself.conn = duckdb.connect(db_path) def execute(self, sql, params=None): return self.conn.execute( sql, params or [] ).fetchall() In other words, miniagent does not force:\nAll data to go through the same database access method.\nInstead:\nOperational Data Business data ↓ SQLAlchemy / SQLite Analytical Data Analytics data ↓ DuckDB And that is perfectly reasonable.\n4. Why Can an Analytical Database Exist Separately? Because databases like:\nSQLite PostgreSQL MySQL are better at:\nOLTP (Online Transaction Processing)\nFor example:\nCreating users Modifying an Agent Saving chat history Permission management While DuckDB leans toward:\nOLAP (Online Analytical Processing)\nFor example:\nAnalyzing CSVs Aggregating millions of rows GROUP BY Statistical reports Ad-hoc analysis So a correct multi-data-source architecture is not:\n\u0026ldquo;Find one database that solves every problem.\u0026rdquo;\nBut rather:\nDifferent data sources take on different responsibilities.\n5. A More Complete Multi-Data-Source Architecture Below is the complete data architecture of miniagent:\nAs shown above, miniagent has two clearly distinct data access routes.\nThe first is for business data:\nRouter ↓ Service ↓ Async*Database / Repository ↓ AsyncBaseDatabase ↓ SQLAlchemy ↓ SQLite ServiceContainer centrally creates the Engine and SessionFactory, then injects them into multiple business database objects, rather than letting each module create its own database connections.\nAsyncBaseDatabase further manages the Session, Commit, Rollback, and Close lifecycle in one place.\nThe second is for analytical data:\nSQL Agent ↓ DuckDBManager ↓ DuckDB DuckDBManager directly wraps the DuckDB connection and SQL execution.\nSo what miniagent truly demonstrates today is:\nflowchart TB A[miniagent] --\u003e B[Business / Runtime] B --\u003e C[Business Data] B --\u003e D[Analytics Data] C --\u003e E[Repository Layer] E --\u003e F[AsyncBaseDatabase] F --\u003e G[SQLAlchemy] G --\u003e H[SQLite] D --\u003e I[SQL Agent] I --\u003e J[DuckDBManager] J --\u003e K[DuckDB] It is not:\n\u0026ldquo;All databases must go through SQLAlchemy.\u0026rdquo;\nBut rather:\nEach data source uses the access method that suits it, while the concrete database implementation stays behind the infrastructure boundary.\nThis is how multi-data-source architecture actually lands in practice.\n6. The Core of Cross-Data-Source Design Is Not \u0026ldquo;Compatibility\u0026rdquo; but \u0026ldquo;Isolation\u0026rdquo; Many people would state the goal as:\nI want to support MySQL, SQLite, and PostgreSQL.\nBut a better goal should be:\nWhen the database changes, the changes stay as much as possible within the infrastructure layer.\nThat is:\nDatabase Changed ↓ Infrastructure ↓ Repository Instead of:\nDatabase Changed ├── Router modified ├── Service modified ├── Tool modified ├── API modified └── Frontend modified This is what we call:\nChange Isolation\nWhat architecture truly wants to solve is the cost of change.\n7. The Repository Layer Is the Database\u0026rsquo;s \u0026ldquo;Firewall\u0026rdquo; You can think of a Repository as:\nA wall between the business world and the database world.\nOn the left:\nBusiness says:\nGive me a User Save an Agent Query the KB On the right:\nDatabase deals with:\nSQL Join Session Transaction Dialect Index Connection The Repository in the middle does the translation.\nFor example:\nService ↓ user_repository.get_by_id(10) ↓ Repository ↓ SQLAlchemy ↓ SQLite / PostgreSQL So a very important AI Rule is:\nServices are not allowed to bypass the Repository and touch the database directly.\nIII. Multi-Data-Source Architecture 1. How Should It Be Layered? You can adopt rules like this: flowchart TB A[Application / Service] --\u003e B[Data Access Abstraction] B --\u003e C[Repository] B --\u003e D[Adapter] B --\u003e E[Client] C --\u003e F[SQL DB] D --\u003e G[DuckDB] E --\u003e H[Vector DB] Where:\nRepository Fits business database CRUD.\nAdapter Fits adapting different protocols or special data sources.\nClient / Manager Fits:\nVector Store External Search Engine The key point is:\nWhat the upper layers see are \u0026ldquo;capabilities\u0026rdquo;, not \u0026ldquo;product names\u0026rdquo;.\n2. When Should You Extract a Unified Interface? For example, if the business layer only needs:\nawait user_repository.get_by_id(id) then you can have:\nUserRepository │ ├── SQLAlchemyUserRepository └── FutureOtherRepository But don\u0026rsquo;t — in the name of \u0026ldquo;we might support ten databases in the future\u0026rdquo; — start by creating:\nAbstractDatabaseFactoryProviderManagerAdapter a giant apparatus like that.\nArchitecture needs extension points, but not over-engineering.\nFor many Python projects:\nService ↓ Repository ↓ SQLAlchemy is already enough to isolate changes like SQLite → PostgreSQL.\n3. What Are the Benefits? Lower database migration cost Suppose:\nDevelopment: SQLite and later production switches to:\nPostgreSQL If the business code makes heavy use of SQLAlchemy\u0026rsquo;s generic capabilities, the changes mainly concentrate on:\nDatabase URL Driver Migration A few dialect differences rather than rewriting all the Services.\nEach database can play to its own strengths For example:\nBusiness transactions → PostgreSQL / SQLite Analytical queries → DuckDB Vector search → Vector Database Caching → Redis instead of using one hammer for every problem.\nEasier testing If a Service only depends on:\nRepository then in tests it can be replaced with:\nFake Repository without actually starting a database.\nDatabase capabilities don\u0026rsquo;t pollute business semantics Business code remains:\nawait agent_service.create(...) instead of:\nSQLite INSERT PostgreSQL ON CONFLICT MySQL UPSERT The business code stays easier to understand.\nBetter suited for AI coding The most dangerous trait of AI is:\nSeeing one working pattern and copying it across the entire project.\nIf the project prescribes:\nRouter → Service → Repository → Database then AI has a clear path to follow.\nOtherwise it will easily do:\nWherever data is needed → connect to the database right there IV. Establishing Multi-Data-Source Architecture Rules for AI You can add the following directly to your Project Rules:\n## Multi-Data-Source Architecture Business code is forbidden from depending directly on specific database implementations. Call chain: Router → Service → Repository / Data Adapter → Database Client / ORM → Data Source Rules: - Routers must not create database connections; - Services must not contain database-specific SQL; - Ordinary business database access goes into Repositories; - Connection, Session, Transaction, and Driver lifecycles belong to the infrastructure layer; - Ordinary CRUD should prefer generic ORM expressions; - Database-specific SQL may only stay in the data access layer; - Scattered database_type checks in the business layer are forbidden; - Different data sources may use different access technologies; - Do not force DuckDB, vector databases, Redis, etc. into a relational ORM. Core goal: What gets unified is the \u0026#34;access boundary\u0026#34;, not forcing all data sources to use the same implementation. Prompting in Practice: Don\u0026rsquo;t Just Tell AI \u0026ldquo;Support PostgreSQL\u0026rdquo; A bad prompt:\nChange this project to support both SQLite and PostgreSQL.\nThe AI will likely start writing:\nif db_type == \u0026#34;sqlite\u0026#34;: ... else: ... and then change things everywhere.\nA more reasonable prompt:\nPlease add PostgreSQL compatibility to the current project. Requirements: 1. First inspect the existing database access boundaries; 2. Router and Service must not be aware of the database type; 3. Ordinary CRUD continues to reuse the existing Repository; 4. SQLAlchemy Engine and SessionFactory are created centrally by the infrastructure layer; 5. Prefer SQLAlchemy generic expressions; 6. Identify SQLite-specific SQL or data types; 7. Database differences may only be encapsulated in infrastructure / repository; 8. Adding new db_type if/else in business code is forbidden; 9. Keep the existing Service APIs unchanged; 10. Provide a checklist of compatibility differences between SQLite and PostgreSQL. This way the AI\u0026rsquo;s goal shifts from:\n\u0026ldquo;Add an if wherever something breaks.\u0026rdquo;\nto:\n\u0026ldquo;Confine database differences to the correct architectural layer.\u0026rdquo;\nWhen Adding a New Data Source, Ask AI a Few Questions First For example, to introduce DuckDB, Redis, or a vector database, you can require the AI to first answer:\nBefore implementing, please answer: 1. What responsibility does the new data source take on? 2. Is it business transactional data or analytical data? 3. Should the existing Repository be reused? 4. Does it need a dedicated Adapter / Manager? 5. What unified capability should the Service see? 6. Which database-specific logic must be isolated? 7. Who manages the Connection lifecycle? 8. Are transactions needed? 9. Is connection pooling needed? 10. Should it really share one implementation with the existing database? The last question is especially important.\nBecause:\nMulti-data-source architecture does not mean making all data sources the same.\nV. How Can miniagent Naturally Evolve in the Future? If the business database later switches from SQLite:\nSQLite ↓ PostgreSQL As long as it keeps the structure:\nService ↓ Repository ↓ SQLAlchemy most business logic never needs to know about the change.\nMeanwhile the system can keep DuckDB for analytics, with Chroma / Vector Store handling vector retrieval, and possibly Redis serving as a shared value cache in the future.\nEventually forming:\nflowchart TB A[Application] --\u003e B[Service Layer] B --\u003e C[Data Access Boundaries] C --\u003e D[Relational DB] C --\u003e E[DuckDB] C --\u003e F[Vector DB] C --\u003e G[Redis] D --\u003e H[PostgreSQL] E --\u003e I[Analytics] F --\u003e J[Retrieval] G --\u003e K[Cache] Upper-layer business code never needs to handle these differences directly.\nSummary The real problem that multi-data-source architecture solves is not:\n\u0026ldquo;How do I write code that connects to five databases at once?\u0026rdquo;\nBut rather:\n\u0026ldquo;When five databases exist, how does the business code stay clean?\u0026rdquo;\nThe truly important principle is:\nBusiness defines what data is needed ↓ The data access layer decides how to obtain it ↓ Infrastructure decides which database to use For ordinary business CRUD:\nService ↓ Repository ↓ ORM ↓ Relational Database For special data sources:\nService / Tool ↓ Dedicated Adapter / Manager ↓ DuckDB / Vector DB / Redis So the one sentence most worth writing into your AI project rules is not:\n\u0026ldquo;The project must support multiple databases.\u0026rdquo;\nBut rather:\nForbid AI from writing a specific database into business logic; what gets unified is the access boundary, not forcing all data sources to use the same implementation.\nThen no matter what sits underneath:\nSQLite PostgreSQL DuckDB Redis Vector Database what actually changes should mainly be the infrastructure — not the entire project.\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0113/","summary":"\u003cp\u003eMany projects start with just one database. So AI naturally writes:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_user\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser_id\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"k\"\u003ewith\u003c/span\u003e \u003cspan class=\"n\"\u003esqlite_session\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"k\"\u003eas\u003c/span\u003e \u003cspan class=\"n\"\u003esession\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"o\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOr even more directly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003econn\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esqlite3\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003econnect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;app.db\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe feature certainly works, but problems will show up later.\u003c/p\u003e\n\u003cp\u003eToday it\u0026rsquo;s \u003ccode\u003eSQLite\u003c/code\u003e, tomorrow it might need to switch to \u003ccode\u003ePostgreSQL\u003c/code\u003e \u0026hellip;\u003c/p\u003e\n\u003cp\u003eAs the codebase grows, the project quickly becomes:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBusiness code tightly coupled to a specific storage technology.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis is exactly the problem that \u003cstrong\u003eMulti-Data-Source Architecture\u003c/strong\u003e aims to solve.\u003c/p\u003e\n\u003chr\u003e","title":"Multi-Data-Source Architecture: Stop Letting AI Lock Your Project to One Database"},{"content":"When AI writes business code, it has a very typical tendency:\nasync def get_user(user_id: int): return await user_db.get_by_id(user_id) Functionally correct. But if every read in the project becomes:\nRead user → Database Read permissions → Database Read model → Database Read Agent → Database Read knowledge base → Database Read config → Database The system soon enters a state where:\nEvery function is correct, yet the overall architecture grows increasingly inefficient.\nThe problem is not merely \u0026ldquo;whether there is a cache\u0026rdquo;, but:\nThe AI never first asks: what I want to reuse — a \u0026ldquo;value\u0026rdquo;, or an \u0026ldquo;already-built runtime object\u0026rdquo;?\nThis is exactly what\u0026rsquo;s most worth borrowing from miniagent\u0026rsquo;s current caching architecture.\nIt doesn\u0026rsquo;t just build one cache — it explicitly splits into two categories:\nObject Cache Object caching Value Cache Value caching These two solve fundamentally different problems.\n📌 Technology Card Cache\nTemporarily stores content that is expensive to obtain and may be reused, in order to reduce repeated construction, repeated computation, or repeated access to the underlying storage.\nObject Cache\nCaches runtime objects that have already been built, such as:\nAgentRunner, Pipeline, Router, Manager.\nIt solves:\nDon\u0026rsquo;t repeatedly create expensive objects.\nValue Cache\nCaches plain data, such as:\npermission sets, query results, JSON, strings, computed results.\nIt solves:\nDon\u0026rsquo;t repeatedly query and recompute.\nMulti-Level Storage\nBased on speed, capacity, cost, and durability, places data at different tiers, for example:\nL1 local memory → L2 Redis → Database.\nHere:\nL1 (Level 1) is usually process memory;\nL2 (Level 2) is usually a shared cache like Redis;\nDatabase serves as the ultimate authoritative data source.\n💡 An Intuitive Way to Understand It: A Cache Is Not Just a \u0026ldquo;Warehouse\u0026rdquo;, but Also an \u0026ldquo;Assembled Machine\u0026rdquo; Imagine a software system as a factory, and the database as the raw-materials warehouse.\nIf every time an employee needs a power drill, they run to the warehouse:\nPick up parts ↓ Assemble the motor ↓ Attach the drill bit ↓ Test it ↓ Use it That\u0026rsquo;s clearly wasteful. The more sensible way is:\nFirst time: Warehouse → Assemble the drill → Use it From then on: Just grab the ready-made drill → Use it This is:\nObject caching.\nAnother case:\nThe employee just wants to check:\nHow much inventory is left today? First trip to the warehouse confirms:\nStock = 128 If others ask the same question shortly after, there\u0026rsquo;s no need for everyone to re-inventory the warehouse.\nYou can just note it down:\nStock cache = 128 This is:\nValue caching.\nSo although the two caches share a similar name, they are fundamentally different:\nObject cache → caches \u0026#34;already-assembled things\u0026#34; Value cache → caches \u0026#34;already-looked-up data\u0026#34; This is exactly miniagent\u0026rsquo;s dual-track caching philosophy.\nWhy Is \u0026ldquo;Just Add Redis\u0026rdquo; Not Enough? Many AIs, when faced with caching problems, swing to another extreme:\n\u0026ldquo;We can add Redis.\u0026rdquo;\nRedis is indeed widely used, but it can\u0026rsquo;t solve every caching problem.\nFor example, in miniagent:\nAgentRunner SmartRouter WebSearchPipeline KBRetrievalPipeline VectorStoreManager These are not plain JSON. Internally they may hold:\nLLM Client Tool instances Repository references asyncio.Lock Database objects Runtime state Pipeline objects Such objects usually:\nCannot be simply serialized Cannot be directly reused across processes Depend on current-process resources So they are better suited to:\nProcess-Local Object Cache\nrather than:\nRedis.set(\u0026#34;agent_runner\u0026#34;, ...) So the first question the caching architecture should answer is not:\nShould we use Redis?\nbut:\nWhat exactly am I caching?\n1. miniagent\u0026rsquo;s Dual-Track Cache Architecture Currently miniagent explicitly maintains two separate cache-management systems inside ServiceContainer:\nself.cache_registry = ObjectCacheRegistry() self.object_cache_invalidator = ObjectCacheInvalidator( self.cache_registry ) from app.infra.cache.store_registry import ( cache_registry as value_cache_registry ) self.value_cache_registry = value_cache_registry 1. Object Cache: Solving \u0026ldquo;Don\u0026rsquo;t Repeatedly Build\u0026rdquo; What is the Object Cache suited for? Typical scenarios:\nAgentRunner WebSearchPipeline SQLAgent SmartRouter KBRetrievalPipeline VectorStoreManager The construction of these Runtime Objects may involve:\nRead database config ↓ Create LLM Client ↓ Load tools ↓ Assemble Pipeline ↓ Bind Repository ↓ Establish Runtime ↓ Complete initialization If every request does this from scratch:\nRequest ↓ build() ↓ use() ↓ destroy Then a lot of CPU, I/O, and initialization cost is wasted on repeated construction.\nSo miniagent\u0026rsquo;s approach is:\nFirst use ↓ No cache ↓ Build the object ↓ Store it in the Object Cache ↓ Reuse it directly from then on The core of the Object Cache: AsyncLazyCache miniagent\u0026rsquo;s object cache core lives at:\napp/runtime/cache/lazy_cache.py Where:\nAsyncLazyCache[K, V] provides:\nget_or_build() The semantics are:\nReturn it if it exists; otherwise build it.\nCore logic:\nasync def get_or_build(self, key, *args, **kwargs): if key in self._store: return self._store[key] ... value = await self._builder( key, *args, **kwargs ) self._store[key] = value return value The flow can be understood as:\nflowchart TB A[Request Runtime Object] --\u003e B[Object Cache] B --\u003e C{Exists?} C --\u003e|Yes| D[Return] C --\u003e|No| E[Builder] E --\u003e F[Build object] F --\u003e G[Write to cache] G --\u003e D The Object Cache Must Handle \u0026ldquo;Build Breakdown\u0026rdquo; Suppose two requests simultaneously need:\nAgentRunner(agent_id=10) and the object hasn\u0026rsquo;t been created yet.\nIf you only do:\nif key not in cache: cache[key] = await build() You may end up with:\nRequest A → build Request B → build Request C → build The same expensive object gets created three times.\nThis is essentially a case of:\nCache Breakdown\nexcept here what\u0026rsquo;s being broken through is not the database, but:\nthe expensive object-construction process.\nminiagent\u0026rsquo;s AsyncLazyCache uses a per-key:\nasyncio.Lock() and double-checked locking:\nasync with self._locks[key]: if key in self._store: return self._store[key] value = await self._builder(...) So:\nflowchart TB A[Request A] --\u003e D[Same Key] B[Request B] --\u003e D C[Request C] --\u003e D D --\u003e E[Single Flight] E --\u003e|Acquires lock| F[\"Request ABuilds the object\"] E --\u003e|Waits| G[\"Request B / CWaiting\"] F --\u003e H[Write to cache] H --\u003e I[\"Request AReturns object\"] H --\u003e J[\"Request B / CDirectly reuse cached object\"] G --\u003e J Here:\nSingle Flight\nmeans:\nFor the same key, only one task at a time is allowed to be responsible for loading or building.\nThis is a very important design in miniagent\u0026rsquo;s object cache.\nWhy Doesn\u0026rsquo;t the Object Cache Usually Rely on TTL? This is where object and value caches clearly diverge.\nFor example:\nAgentRunner doesn\u0026rsquo;t become invalid just because:\n1 hour is up What typically invalidates it is:\nAgent config changed LLM changed Tool changed KB changed Embedding changed Router config changed In other words:\nAn object\u0026rsquo;s validity mainly depends on \u0026ldquo;whether its dependency config has changed\u0026rdquo;.\nSo it\u0026rsquo;s better suited to:\nEvent-Driven Invalidation\nrather than:\nTTL (Time To Live)\nHow Does miniagent Invalidate Object Caches? miniagent specifically designed:\nCacheInvalidationService Located at:\napp/runtime/cache/invalidation.py For example, when an Agent config changes:\ndef on_agent_changed(self, agent_id): if agent_id: self.registry.invalidate( CacheType.AGENT_RUNNER, agent_id ) So:\nAgent Changed ↓ AgentRunner is now stale ↓ Invalidate ↓ Next call ↓ Rebuild This is the classic:\nConfig-driven runtime rebuild.\nObject-cache invalidation is really \u0026ldquo;dependency-graph governance\u0026rdquo; For example, an LLM config change.\nIt may affect not only:\nLLM Runtime but also:\nWebSearchPipeline SQLAgent AgentRunner KBRetrievalPipeline So miniagent uniformly does:\ndef on_llm_changed(self): self.registry.invalidate_all( CacheType.WEB_SEARCH_PIPELINE ) self.registry.invalidate_all( CacheType.SQL_AGENT ) self.registry.invalidate_all( CacheType.AGENT_RUNNER ) self.registry.invalidate_all( CacheType.KB_RETRIEVAL_PIPELINE ) Essentially: flowchart TB A[LLM Config Changed] --\u003e B[Dependency Graph] B --\u003e C[Web Search] B --\u003e D[SQL Agent] B --\u003e E[Agent Runner] B --\u003e F[KB Retrieval] This is far safer than:\ncache.clear() Because it knows:\nWhich objects depend on which configs.\n2. Value Cache: Solving \u0026ldquo;Don\u0026rsquo;t Repeatedly Query Data\u0026rdquo; The value cache is the one we\u0026rsquo;re most familiar with.\nIt caches:\nPermission Set Query Result ... Its core goal is:\nAvoid repeated database queries or repeated computation.\nThe Value Cache Uses Cache-Aside A typical value-cache flow: flowchart TB A[Request] --\u003e B[Value Cache] B --\u003e C{Hit?} C --\u003e|Yes| D[Return] C --\u003e|No| E[Database] E --\u003e F[Obtain value] F --\u003e G[Write to cache] G --\u003e D This pattern is usually called:\nCache-Aside Pattern\nThe application itself controls:\nRead Cache first ↓ On miss, read Database ↓ Then write Cache miniagent\u0026rsquo;s Value Cache Infrastructure It lives at:\napp/infra/cache/ ├── factory.py ├── memory.py └── store_registry.py Created uniformly through:\ncreate_cache_backend(...) For example:\ncreate_cache_backend( namespace=\u0026#34;auth\u0026#34;, backend_type=\u0026#34;memory\u0026#34;, ) Currently it supports:\nMemoryCacheStore While reserving an interface for the future:\nRedis This means business code depends on:\nthe caching capability\nrather than:\na specific caching product.\nThe Value Cache Needs LRU The easiest way for a value cache to run out of control is:\ncache[key] = value and then never delete anything.\nSo:\n100 1000 10000 100000 ... keeps growing.\nSo miniagent\u0026rsquo;s MemoryCacheStore uses:\nLRU (Least Recently Used)\nself._cache = LRUCache( maxsize=max_size ) When the cache is full:\nThe least-recently-used data is evicted first.\nThe Value Cache Needs TTL The value cache also faces:\nData can go stale.\nFor example, user permissions.\nThe database has been updated, but the cache still holds the old value.\nSo miniagent provides:\nmset_with_ttl() mget_ttl() Through:\nTTL (Time To Live)\nit limits the maximum usable time of the data.\nFor example:\nTTL = 3600 seconds After expiration:\nCache Miss ↓ Re-query the database ↓ Re-cache This is very reasonable for ordinary data caching.\nminiagent\u0026rsquo;s Permission Cache Is a Typical Value Cache In AuthPermission:\ncached = self._cache.mget_ttl( [self._cache_key(user_id)] )[0] if cached is not None: return self._decode(cached) return await self._load_permissions(user_id) That is:\nflowchart TB A[Permission Request] --\u003e B[Value Cache] B --\u003e C{Hit?} C --\u003e|Yes| D[Return permissions] C --\u003e|No| E[Database] E --\u003e F[User → Role → Permission] F --\u003e G[Write to cache] G --\u003e D This avoids:\nRe-querying the RBAC permission chain on every API request.\nA Value Cache Needs Not Only TTL, but Also Active Invalidation TTL alone isn\u0026rsquo;t enough.\nFor example, an admin just revoked a user\u0026rsquo;s permissions.\nIf:\nTTL = 3600 seconds The old permissions could theoretically persist for nearly an hour.\nSo you also need:\nInvalidate Cache\nWhen permissions change:\nDatabase Update ↓ Invalidate Value Cache ↓ Next request ↓ Cache Miss ↓ Reload latest permissions So a complete value cache must consider:\nRead Write TTL Invalidate Capacity Stats rather than only:\nget / set The Value Cache Also Faces Cache Penetration Suppose someone repeatedly requests:\nuser_id = 999999999 The cache doesn\u0026rsquo;t have it:\nMISS The database doesn\u0026rsquo;t have it either.\nNext time:\nMISS ↓ Database Still nothing.\nThis is:\nCache Penetration\nThat is:\nQuerying data that doesn\u0026rsquo;t exist in the first place, causing requests to keep passing through the cache and hitting the database.\nHow to Handle Cache Penetration? One simple approach:\nNegative Cache (empty-value caching)\nFor example:\nuser:999999999 = NOT_FOUND TTL = 60 seconds Next time:\nCache Hit ↓ NOT_FOUND ↓ Return directly The database doesn\u0026rsquo;t need to be queried again.\nThe TTL for empty-value caching should usually be shorter.\nFor example:\nNormal value: 3600 seconds Empty value: 60 seconds To avoid blocking newly created data for too long with the stale empty value.\nThe Value Cache Also Faces Hot-Key Breakdown Suppose:\nsystem_config is a high-frequency hot key.\nNormally:\n1000 Requests ↓ Cache Suddenly the TTL expires:\nCache Expired So:\nRequest A ─┐ Request B ─┤ Request C ─┤ ... ├──→ Database Request N ─┘ This is:\nCache Breakdown\nThe solution can still use:\nSingle Flight / Per-Key Lock\nThat is:\nLots of misses ↓ Only one request with the same key is allowed to go back to source ↓ Other requests wait ↓ Cache is rebuilt ↓ All return uniformly miniagent\u0026rsquo;s Object Cache already natively embodies this Single-Flight idea.\nIn the future, if certain value caches become high-concurrency hot spots, the same approach can be reused.\nCache Avalanche Is Also More Typical of Value Caches If the AI uniformly sets for all caches:\nttl = 3600 And a lot of data is written around the same time:\nWritten at 11:00 ↓ Expires en masse at 12:00 You may get:\nCache Avalanche\nThat is:\nA large number of caches expire at once, and a flood of requests instantly hits the database.\nMitigation methods include:\nTTL Jitter Random TTL variation Multi-Level Cache Multi-tier caching Rate Limiting Throttling 3. Why Is the Object Cache Unsuitable for Redis, While the Value Cache Is a Great Fit? Look at the simplest comparison.\nAgentRunner May contain:\nLLM Client Tool asyncio.Lock Repository Pipeline Runtime State Characteristics:\nComplex Not directly serializable Depends on the current process Suited to:\nLocal Memory Permission Set For example:\n[ \u0026#34;system:user:list\u0026#34;, \u0026#34;system:user:create\u0026#34; ] Characteristics:\nSimple Serializable Shareable across processes Suited to:\nMemory ↓ Redis So when miniagent scales horizontally in the future, the more reasonable approach is:\nObject Cache → Each process still keeps its own Runtime Value Cache → Can evolve into a shared Redis cache That is the design that respects the nature of the objects.\n4. miniagent Also Keeps the Two Caches\u0026rsquo; Registries Separate Object Cache Registry Responsible for:\nWhich Runtime Caches exist? Which Runtime Object to invalidate? Invalidate all? Invalidate by condition? Inspect the Runtime Cache status? Corresponds to:\nCacheRegistry Supports:\ninvalidate invalidate_all invalidate_where stats list_names Value Cache Registry Responsible for:\nWhich namespaces exist? What the underlying backend is? How many keys right now? What\u0026#39;s the hit rate? Which keys to delete? Which namespace to clear? Corresponds to:\nCacheStoreRegistry The responsibilities of these two registries are in fact very clear:\nObject Registry → Runtime lifecycle governance Value Registry → Key-Value data governance 5. Caches Must Be Observable Another thing AI easily overlooks when adding caches:\nIs the cache actually working?\nYou should at least know:\nHits Misses Hit Rate Current Size TTL Expirations miniagent\u0026rsquo;s MemoryCacheStore already maintains:\nself._hits self._misses self._ttl_expirations And through:\nget_stats() returns:\ncurrent_size hits misses hit_rate ttl_expirations So a cache is not:\n\u0026ldquo;It should feel a bit faster now.\u0026rdquo;\nIt should be able to answer:\nHow much was actually hit?\nScene 1: Everything Queries the Database Directly return await repository.get(id) Problem:\nThe AI never judged whether the data is high-frequency, expensive, or already cached.\nScene 2: A Complex Runtime Gets Re-Created Every Time runner = await build_agent_runner(agent_id) Every request rebuilds.\nProblem:\nThe AI didn\u0026rsquo;t realize this isn\u0026rsquo;t an ordinary object, but a reusable Runtime.\nScene 3: All Caches Become One dict cache = {} Then:\nAgentRunner Permission Search Result Config all get stuffed in.\nProblem:\nObject lifecycles and data lifecycles are completely different, yet get conflated.\nScene 4: See a Cache, Reach for Redis \u0026#34;Just add Redis.\u0026#34; Problem:\nRuntime Objects are simply not suitable for cross-process serialization.\nScene 5: Cache but Never Invalidate cache[key] = value And then never touch it again.\nResult:\nThe database changed; the cache is still alive.\nScene 6: Mechanical TTL on the Object Cache Too For example:\nAgentRunner TTL = 1h Problem:\nThe Agent config didn\u0026rsquo;t change, yet it gets rebuilt for nothing.\nOr:\nThe Agent config changed long ago, but the TTL hasn\u0026rsquo;t expired yet.\nThe correct approach should be:\nConfig change → Precise invalidation Scene 7: All Value Caches Are Treated the Same TTL = 3600 max_size = 1000 Copied into every scenario.\nBut in reality:\nPermissions Search results Config Temporary computation results have completely different access patterns.\nThe caching strategy shouldn\u0026rsquo;t be identical either.\n2. Establish Clear Caching Rules for the AI You can add the following to your Project Rules.\n## Cache Architecture miniagent uses two different caching systems: 1. Object Cache 2. Value Cache Never conflate their responsibilities. ### Object Cache The Object Cache stores expensive runtime objects, such as: - AgentRunner - Pipelines - Routers - Vector store managers - Runtime components Rules: - Use AsyncLazyCache. - Use lazy construction. - Use the single-flight pattern to prevent duplicate concurrent construction. - Keep runtime objects process-local. - Do not serialize runtime objects into Redis. - Prefer event-driven invalidation. - When a dependency config changes, invalidate the affected runtime objects. ### Value Cache The Value Cache stores serializable values, such as: - Permission sets - Query results - Computed results - Simple data objects Rules: - Use the shared cache backend abstraction. - Use namespaces to isolate different domains. - Apply capacity limits, e.g. LRU (Least Recently Used). - Use TTL (Time To Live) where data may go stale. - Invalidate relevant keys after important writes. - Consider negative caching for missing data. - Protect hot keys from cache breakdown when necessary. - A distributed backend (e.g. Redis) should belong here. ### Storage Hierarchy The Value Cache may evolve into: L1 Memory → L2 Redis → Database The Object Cache is an independent runtime-lifecycle system, and should not be treated as another L1/L2 value-cache layer. It can be summarized as:\nComplex runtime objects go through the Object Cache; ordinary data goes through the Value Cache.\nInvalidate the object cache on config changes; invalidate the value cache via TTL and data changes.\nJust because both are called \u0026ldquo;Cache\u0026rdquo; doesn\u0026rsquo;t mean the AI should handle them with the same logic.\n3. Putting It into Prompts: Don\u0026rsquo;t Just Say \u0026ldquo;Add a Cache\u0026rdquo; Bad prompt:\nAdd a cache to this feature.\nThe AI has no idea which kind it should be.\nA better prompt:\nFirst determine whether this scenario calls for the Object Cache or the Value Cache. If you\u0026#39;re caching a Runtime Object that is expensive to build: 1. Reuse the existing AsyncLazyCache; 2. Use get_or_build; 3. Preserve Single Flight; 4. Don\u0026#39;t introduce TTL as the primary invalidation method; 5. Clearly state which configs it depends on; 6. On config changes, precisely invalidate via ObjectCacheInvalidator. If you\u0026#39;re caching ordinary serializable data: 1. Reuse the existing Value Cache Backend; 2. Use an independent namespace; 3. Clearly state the key; 4. Clearly state max_size; 5. Clearly state the TTL; 6. Clearly state the invalidate path after data changes; 7. Determine whether cache penetration exists; 8. Determine whether a hot key carries breakdown risk; 9. Don\u0026#39;t privately create dict/LRU caches inside business code. This way, before doing anything, the AI first makes the single most important architectural decision:\nAm I caching an object, or a value?\nSummary What caching and multi-level storage really need to correct is not only:\nAI blindly querying the database.\nIt also includes another equally common problem:\nAI blindly re-creating expensive objects.\nSo in miniagent, the more accurate caching philosophy is:\nObject Cache solves \u0026#34;don\u0026#39;t repeatedly build\u0026#34; Value Cache solves \u0026#34;don\u0026#39;t repeatedly query\u0026#34; The object cache uses:\nAsyncLazyCache + Lazy Build + Single Flight + Event Invalidation to manage the Runtime Object lifecycle.\nThe value cache uses:\nCache Backend + Namespace + LRU + TTL + Invalidate + Stats to reduce database and computation pressure.\nAnd the Value Cache can naturally evolve in the future into:\nL1 Memory ↓ L2 Redis ↓ Database That is a complete and clear caching and multi-level storage architecture.\nFor AI programming, what should truly be written into the rules is not:\n\u0026ldquo;Remember to use caching.\u0026rdquo;\nbut:\nFirst determine whether you\u0026rsquo;re repeatedly \u0026ldquo;building objects\u0026rdquo; or repeatedly \u0026ldquo;querying data\u0026rdquo;; then use the corresponding cache system, rather than casually stuffing a dict into the business code.\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0112/","summary":"\u003cp\u003eWhen AI writes business code, it has a very typical tendency:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003easync\u003c/span\u003e \u003cspan class=\"k\"\u003edef\u003c/span\u003e \u003cspan class=\"nf\"\u003eget_user\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser_id\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"nb\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e):\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003euser_db\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eget_by_id\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser_id\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFunctionally correct. But if every read in the project becomes:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRead user → Database\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRead permissions → Database\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRead model → Database\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRead Agent → Database\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRead knowledge base → Database\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eRead config → Database\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe system soon enters a state where:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eEvery function is correct, yet the overall architecture grows increasingly inefficient.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe problem is not merely \u0026ldquo;whether there is a cache\u0026rdquo;, but:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eThe AI never first asks: what I want to reuse — a \u0026ldquo;value\u0026rdquo;, or an \u0026ldquo;already-built runtime object\u0026rdquo;?\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis is exactly what\u0026rsquo;s most worth borrowing from \u003ca href=\"https://github.com/liupras/miniagent\"\u003eminiagent\u003c/a\u003e\u0026rsquo;s current caching architecture.\u003c/p\u003e\n\u003cp\u003eIt doesn\u0026rsquo;t just build one cache — it explicitly splits into two categories:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eObject Cache\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eObject caching\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eValue Cache\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eValue caching\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThese two solve fundamentally different problems.\u003c/p\u003e\n\u003chr\u003e","title":"Caching and Multi-Level Storage: Don't Let AI Blindly Hit the Database"},{"content":"In real-world development, we often need to use multiple Git accounts on the same computer, for example:\nPersonal GitHub account Company GitHub account GitLab account Gitee account If you simply use the default Git configuration, you can easily run into these problems:\nUsing the wrong account when running git push Commit history showing up under a different account Not knowing which SSH Key to use Frequently modifying user.name and user.email across different projects SSH configurations for GitHub, GitLab, and other platforms interfering with each other The most stable solution to these problems is:\nOne account per SSH Key, distinguish accounts through SSH Config, and determine commit identity through project-level Git Config.\nBelow, using Windows as an example, we\u0026rsquo;ll build a complete Git multi-account workflow.\n1. Understanding the Two Identities in Git Multi-Account Setup Before configuring, you need to distinguish between two easily confused concepts.\n1. Git Commit Identity Every Git commit records:\nuser.name user.email For example:\ngit config user.name \u0026#34;zhangsan\u0026#34; git config user.email \u0026#34;zhangsan@example.com\u0026#34; What it determines is:\nWho submitted this commit.\n2. Git Repository Authentication Identity When executing:\ngit pull git push Servers like GitHub and GitLab also need to verify:\nDo you have permission to access this repository?\nIf using SSH, this identity is determined by the SSH Key.\nTherefore:\nGit Config ↓ Determines who the commit is from SSH Key ↓ Determines which account is used to access the remote repository These two identities need to be configured separately.\n2. Viewing Current Git Configuration First, view the global configuration:\ngit config --global --list Pay attention to:\nuser.name=xxx user.email=xxx You can also view them separately:\ngit config --global user.name git config --global user.email If you previously had only one Git account, you likely already have a global identity configured:\ngit config --global user.name \u0026#34;your-name\u0026#34; git config --global user.email \u0026#34;your-email@example.com\u0026#34; For a multi-account environment, I recommend:\nDon\u0026rsquo;t rely on a single unified global user.name / user.email. Instead, set the corresponding identity in each project.\n3. Generating Different SSH Keys for Different Accounts Assume you now have two GitHub accounts:\nPersonal account: personal Work account: work Open PowerShell or Git Bash.\nFirst, check existing SSH Keys:\nls ~/.ssh The actual Windows directory is typically:\nC:\\Users\\YourUsername\\.ssh Then create two separate keys.\nPersonal account:\nssh-keygen -t ed25519 -C \u0026#34;personal@example.com\u0026#34; Save as:\n~/.ssh/id_ed25519_personal Work account:\nssh-keygen -t ed25519 -C \u0026#34;work@example.com\u0026#34; Save as:\n~/.ssh/id_ed25519_work The final directory should look like:\n.ssh/ ├── id_ed25519_personal ├── id_ed25519_personal.pub ├── id_ed25519_work └── id_ed25519_work.pub Where:\nid_ed25519_xxx is the private key — never upload or share it with anyone.\nAnd:\nid_ed25519_xxx.pub is the public key, which can be added to code hosting platforms like GitHub and GitLab.\n4. Adding SSH Public Keys to Code Hosting Platforms View the personal account public key:\ncat ~/.ssh/id_ed25519_personal.pub View the work account public key:\ncat ~/.ssh/id_ed25519_work.pub Copy the corresponding content and add each to the respective Git account\u0026rsquo;s SSH Keys.\nFor example:\npersonal GitHub ↑ id_ed25519_personal.pub work GitHub ↑ id_ed25519_work.pub This way, each account has its own independent SSH identity.\n5. Using SSH Config to Distinguish Different Accounts This is the most critical step in the entire multi-account configuration.\nCreate or edit:\nC:\\Users\\YourUsername\\.ssh\\config Note that the filename is simply:\nconfig No .txt extension.\nAssuming both accounts use GitHub, you can configure:\n# Personal GitHub Host github-personal HostName github.com User git IdentityFile ~/.ssh/id_ed25519_personal IdentitiesOnly yes # Work GitHub Host github-work HostName github.com User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes The most important parts here are:\nHost github-personal and:\nHost github-work These are SSH aliases we define ourselves.\nIn reality, both ultimately access:\ngithub.com But SSH will select different keys based on different Host aliases.\nThat is:\ngithub-personal ↓ github.com ↓ id_ed25519_personal And:\ngithub-work ↓ github.com ↓ id_ed25519_work This solves the problem:\nHow to use two different accounts on the same github.com.\n6. Testing Both SSH Accounts After configuration, test each separately.\nPersonal account:\nssh -T git@github-personal Work account:\nssh -T git@github-work If configured correctly, GitHub will return something like:\nHi username! You\u0026#39;ve successfully authenticated... Check whether the returned username matches the expected account.\nIf the two commands are recognized as two different GitHub users, the SSH multi-account configuration is successful.\n7. Choosing the Correct Account When Cloning Repositories A standard GitHub SSH URL is typically:\ngit@github.com:user/repository.git After configuring multi-account, you no longer use:\ngithub.com directly, but instead use the Host defined in your SSH Config.\nFor example, for a personal project:\ngit clone git@github-personal:personal-user/my-project.git For a work project:\ngit clone git@github-work:company/my-project.git Note that:\ngithub-personal github-work are not real domain names.\nSSH will automatically map them to:\ngithub.com based on:\n~/.ssh/config while selecting the correct SSH Key.\n8. How to Switch Accounts for Existing Repositories If a project has already been cloned, you can view the current remote URL:\ngit remote -v You might see:\norigin git@github.com:company/project.git If this is a work account project, you can change it to:\ngit remote set-url origin git@github-work:company/project.git Check again:\ngit remote -v It should now show:\norigin git@github-work:company/project.git From now on, when you run:\ngit pull git push SSH will automatically use:\nid_ed25519_work for authentication.\n9. Configuring the Correct Git Identity for Each Project SSH solved the problem of \u0026ldquo;which account to use for repository access.\u0026rdquo;\nNext, we need to solve:\nWhose name and email should the commit display?\nEnter the personal project:\ncd my-personal-project Set:\ngit config user.name \u0026#34;personal-name\u0026#34; git config user.email \u0026#34;personal@example.com\u0026#34; Enter the company project:\ncd my-work-project Set:\ngit config user.name \u0026#34;work-name\u0026#34; git config user.email \u0026#34;work@company.com\u0026#34; Note that you should not add --global here.\nFor example:\ngit config user.email \u0026#34;work@company.com\u0026#34; means:\nThis only applies to the current repository.\nWhereas:\ngit config --global user.email \u0026#34;work@company.com\u0026#34; would affect all Git projects on the entire computer.\nYou can use the following commands to confirm the final identity used in the current project:\ngit config user.name git config user.email If you want to further confirm where the configuration comes from:\ngit config --show-origin --get user.name git config --show-origin --get user.email 10. Recommended Complete Multi-Account Structure Ultimately, a clear Windows Git multi-account structure looks like this:\nWindows │ ├── ~/.ssh/ │ │ │ ├── id_ed25519_personal │ ├── id_ed25519_personal.pub │ │ │ ├── id_ed25519_work │ ├── id_ed25519_work.pub │ │ │ └── config │ ├── Personal Project │ │ │ ├── Git Config │ │ ├── user.name = personal-name │ │ └── user.email = personal@example.com │ │ │ └── origin │ └── git@github-personal:user/project.git │ └── Work Project │ ├── Git Config │ ├── user.name = work-name │ └── user.email = work@company.com │ └── origin └── git@github-work:company/project.git This forms two independent control layers:\nProject Git Config ↓ Controls commit identity Remote URL ↓ SSH Config Host ↓ IdentityFile ↓ Controls remote repository authentication identity Once you understand this, Git multi-account is actually quite simple.\n11. Using GitHub + GitLab + Gitee Simultaneously If instead of two GitHub accounts, you have multiple different platforms, the configuration method is the same.\nFor example:\nHost github-personal HostName github.com User git IdentityFile ~/.ssh/id_ed25519_personal IdentitiesOnly yes Host github-work HostName github.com User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes Host gitlab-work HostName gitlab.com User git IdentityFile ~/.ssh/id_ed25519_gitlab IdentitiesOnly yes Host gitee-personal HostName gitee.com User git IdentityFile ~/.ssh/id_ed25519_gitee IdentitiesOnly yes Then simply choose different URLs based on the repository:\ngit@github-personal:user/project.git git@github-work:company/project.git git@gitlab-work:company/project.git git@gitee-personal:user/project.git So this solution doesn\u0026rsquo;t depend on any specific platform — it\u0026rsquo;s essentially using SSH Config to manage multiple SSH identities.\n12. Further Optimization: Auto-Switching Git Identity by Directory If you have many projects, manually running for each one:\ngit config user.name ... git config user.email ... is still tedious.\nGit natively supports includeIf, which can automatically load different configurations based on the project\u0026rsquo;s directory.\nFor example, let\u0026rsquo;s define:\nD:\\project\\personal\\ D:\\project\\work\\ to store personal and company projects respectively.\nEdit:\n~/.gitconfig Configure:\n[includeIf \u0026#34;gitdir:D:/project/personal/\u0026#34;] path = ~/.gitconfig-personal [includeIf \u0026#34;gitdir:D:/project/work/\u0026#34;] path = ~/.gitconfig-work Create:\n~/.gitconfig-personal Content:\n[user] name = personal-name email = personal@example.com Then create:\n~/.gitconfig-work Content:\n[user] name = work-name email = work@company.com This way, as long as a project is located in:\nD:\\project\\personal\\ Git will automatically use the personal identity.\nIf the project is located in:\nD:\\project\\work\\ It will automatically use the work identity.\nUltimately, you can achieve:\nWindows │ ┌─────────┴─────────┐ ↓ ↓ D:\\project\\personal D:\\project\\work │ │ ↓ ↓ personal Git Config work Git Config │ │ ↓ ↓ github-personal github-work │ │ ↓ ↓ Personal SSH Key Work SSH Key This is the approach I recommend for long-term use.\n13. Daily Workflow After a one-time configuration, daily development is actually very simple.\nPersonal project:\ncd D:\\project\\personal git clone git@github-personal:user/project.git Work project:\ncd D:\\project\\work git clone git@github-work:company/project.git Then use normally:\ngit add . git commit -m \u0026#34;update\u0026#34; git pull git push That\u0026rsquo;s it.\nNo need to switch GitHub login accounts daily, and no need to repeatedly modify SSH Keys.\nSummary Using multiple Git accounts simultaneously on a single Windows computer essentially requires solving two problems:\nFirst, whose identity does the commit use?\nControlled by:\ngit config user.name git config user.email Second, whose permission does Push use?\nControlled by:\nSSH Config ↓ Host Alias ↓ IdentityFile Therefore, a stable multi-account solution can be summarized as:\nOne Git account ↓ One SSH Key ↓ One SSH Host Alias ↓ One corresponding Remote URL Combined with:\nGit includeIf ↓ Based on project directory ↓ Automatically select user.name / user.email You can build a Git multi-account development environment on Windows that requires virtually no manual switching.\nConfigure once, and personal projects, company projects, and different code repositories like GitHub, GitLab, and Gitee can all be used simultaneously without interfering with each other.\n","permalink":"http://www.wfcoding.com/en/articles/programmer/01/","summary":"\u003cp\u003eIn real-world development, we often need to use multiple Git accounts on the same computer, for example:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ePersonal GitHub account\u003c/li\u003e\n\u003cli\u003eCompany GitHub account\u003c/li\u003e\n\u003cli\u003eGitLab account\u003c/li\u003e\n\u003cli\u003eGitee account\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIf you simply use the default Git configuration, you can easily run into these problems:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eUsing the wrong account when running \u003ccode\u003egit push\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eCommit history showing up under a different account\u003c/li\u003e\n\u003cli\u003eNot knowing which SSH Key to use\u003c/li\u003e\n\u003cli\u003eFrequently modifying \u003ccode\u003euser.name\u003c/code\u003e and \u003ccode\u003euser.email\u003c/code\u003e across different projects\u003c/li\u003e\n\u003cli\u003eSSH configurations for GitHub, GitLab, and other platforms interfering with each other\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe most stable solution to these problems is:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eOne account per SSH Key, distinguish accounts through SSH Config, and determine commit identity through project-level Git Config.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eBelow, using Windows as an example, we\u0026rsquo;ll build a complete Git multi-account workflow.\u003c/p\u003e\n\u003chr\u003e","title":"Git Multi-Account Configuration Guide on Windows"},{"content":"In AI programming, there is one category of code that easily \u0026ldquo;grows and grows messier\u0026rdquo;:\nif user.role != \u0026#34;admin\u0026#34;: raise HTTPException(status_code=403) Switch to another endpoint, and the AI writes:\nif \u0026#34;user:delete\u0026#34; not in user.permissions: raise ForbiddenError() Yet another endpoint:\nif current_user.id != owner_id and not current_user.is_admin: raise HTTPException(status_code=403) Each snippet looks defensible on its own. But once dozens of variations of these ifs appear across the project, the permission system has effectively gone out of control.\nThe mature solution is not \u0026ldquo;make the AI write its if-statements more carefully\u0026rdquo;, but to establish a unified:\nAuthentication + permission model + permission enforcement mechanism\nThe most common permission model here is RBAC (Role-Based Access Control).\n📌 Technology Card Authentication\nAnswers the question:\n\u0026ldquo;Who are you?\u0026rdquo;\nFor example, verifying the login account, or whether a JWT token is valid.\nAuthorization\nAnswers the question:\n\u0026ldquo;What are you allowed to do?\u0026rdquo;\nFor example, whether a user can delete an Agent, modify a knowledge base, or manage users.\nRBAC / Role-Based Access Control\nInstead of attaching a pile of permissions to each user directly, it establishes a unified grant relationship through:\nUser → Role → Permission\nFor example:\nZhang San ↓ System Administrator ↓ user:list user:create user:update user:delete Rather than checking separately in dozens of endpoints:\nif username == \u0026#34;zhangsan\u0026#34;: ... or:\nif role == \u0026#34;admin\u0026#34;: ... 💡 An Intuitive Way to Understand It: Access Control Is Like a Company Badge System Imagine a company where employees swipe their badge cards when entering the building.\nThe system first needs to determine:\nIs this card genuine? Has it expired? Who does it belong to? Is this employee still on staff? That is:\nAuthentication\nOnce identity is confirmed, further checks continue as the employee enters different areas:\nCan a regular employee enter the office area? Yes. Can a regular employee enter the server room? No. Can ops staff enter the server room? Yes. Can finance staff open the finance system? Yes. That is:\nAuthorization\nIf designed sensibly, the company doesn\u0026rsquo;t tape a note next to every door saying:\nIf Zhang San comes, open the door If Li Si comes, open it too If Wang Wu comes, don\u0026#39;t open If the boss comes, open everything Because once there are many employees, that rule set immediately spirals out of control.\nThe more sensible approach is:\nEmployee ↓ Role ↓ Permission ↓ Resource For example:\nZhang San → Ops staff → server:manage Li Si → Finance staff → finance:view Wang Wu → Regular employee → office:access That is RBAC.\n1. Why Does AI Botch Permission Systems So Easily? Because AI is very good at solving the problem right in front of it.\nYou tell it:\nAdd a permission check to the delete-user endpoint.\nIt most easily generates:\nif not current_user.is_admin: raise HTTPException(status_code=403) Feature done.\nBut next time you ask it:\nAdd a permission check to the delete-Agent endpoint.\nIt may generate:\nif \u0026#34;agent:delete\u0026#34; not in user.permissions: raise HTTPException(status_code=403) Then ask it to protect the knowledge base:\nif user.role not in [\u0026#34;admin\u0026#34;, \u0026#34;manager\u0026#34;]: raise HTTPException(status_code=403) Eventually the project may simultaneously contain:\nis_admin role == \u0026#34;admin\u0026#34; role in [...] permissions permission_codes user_type is_super six or seven different permission-checking approaches.\nThis is the classic case of:\nFunctionally correct, architecturally out of control.\n2. Architectural Rules 1. First, Separate Authentication from Authorization A unified auth system must begin with a very important boundary:\nAuthentication Auth: who are you? ↓ Authorization Auth: what can you do? Don\u0026rsquo;t blend the two into one giant function. A clean architecture looks like:\nClient Request ↓ Bearer Token ↓ JWT Verification ↓ Resolve User ↓ Check User Status ↓ Authenticated User ID ↓ Permission Resolution ↓ Permission Check ↓ Router ↓ Service Where:\nBearer Token\nis usually carried in the HTTP request header:\nAuthorization: Bearer xxxxx And:\nJWT (JSON Web Token)\nis responsible for proving:\nWho issued this token? Has it been tampered with? Has it expired? Which user does it correspond to? RBAC, meanwhile, is responsible for:\nWhich roles does this user hold? Which permissions do those roles hold? Which permission does the current endpoint require? The two have completely different responsibilities.\n2. JWT Issuance Must Have Exactly One Standard Implementation Another common problem in AI projects is that token issuance logic gets implemented repeatedly.\nFor example:\npayload = { \u0026#34;username\u0026#34;: username, \u0026#34;exp\u0026#34;: datetime.utcnow() + timedelta(hours=1) } token = jwt.encode(payload, SECRET) Somewhere else:\npayload = { \u0026#34;sub\u0026#34;: user.id, \u0026#34;expire\u0026#34;: ... } And a third place:\njwt.encode( {\u0026#34;user\u0026#34;: username}, key, algorithm=\u0026#34;HS256\u0026#34; ) Before long you end up with:\nDifferent fields Different expiration times Different algorithms Different ways of reading the signing key Different verification logic So you should explicitly require the AI to follow:\nToken creation, parsing, and verification must go through the unified JWT component only.\nIn miniagent, this responsibility is centralized in:\napp/core/security/jwt_auth.py JWTAuth.create_token() uniformly generates the payload:\npayload = { \u0026#34;sub\u0026#34;: username, \u0026#34;exp\u0026#34;: expire, \u0026#34;iat\u0026#34;: datetime.now(timezone.utc), \u0026#34;type\u0026#34;: token_type } Where:\nsub: Subject, the token principal; exp: Expiration Time; iat: Issued At; type: the token type. It then uniformly calls:\njwt.encode( payload, self.secret_key, algorithm=self.algorithm ) to complete issuance.\nThis means miniagent never needs the Login Router, Admin Router, or User Router each to figure out JWT generation on their own.\n3. Token Verification Must Also Be Centralized Unifying issuance is not enough — verification must not be scattered either.\nThe wrong way is:\ntry: payload = jwt.decode(...) except: ... written separately in dozens of endpoints.\nBecause token verification involves at least:\nIs the signature valid? Has it expired? Which algorithm is used? What is the subject field? How are exceptions handled? In miniagent, JWTAuth.verify_token() uniformly calls:\npayload = jwt.decode( token, self.secret_key, algorithms=[self.algorithm], options={ \u0026#34;verify_exp\u0026#34;: True, \u0026#34;verify_signature\u0026#34;: True } ) After verification passes, it uniformly reads:\nusername = payload.get(\u0026#34;sub\u0026#34;) If the token is expired or invalid, a failure result is returned uniformly.\nSo the whole project only needs to acknowledge one fact:\nJWTAuth ├─ create_token() └─ verify_token() instead of:\nRouter A → decodes on its own Router B → decodes on its own Service C → decodes on its own Middleware D → writes decode yet again 4. Successful Authentication Does Not Mean Having Permission This is a point beginners easily confuse. A successful JWT verification only proves:\nThis request corresponds to a legitimate identity.\nIt does not prove:\nThis user can do anything.\nFor example, a regular user logs in successfully:\nJWT ✅ But tries to delete another user:\nuser:delete ❌ So after authentication, the authorization phase still follows.\nIn miniagent, this part is handled centrally by:\nAuthPermission Its resolve_user_id() flow is:\nToken ↓ JWT verify ↓ username ↓ Look up the user ↓ Check is_active ↓ user_id If the token is invalid, the user doesn\u0026rsquo;t exist, or the account has been disabled, the request is uniformly rejected.\nThis design matters, because authentication must not trust the token alone.\nFor example:\nUser logged in yesterday ↓ Obtained a valid token ↓ Admin disabled the account today ↓ The old token has not yet expired If the system only verified the JWT signature, that user could theoretically keep accessing the system.\nminiagent additionally reads the user\u0026rsquo;s status from the database, so disabling an account takes real effect.\n5. The Core of RBAC: Never Write Permission Checks Directly Against Users The simplest RBAC relationship is:\nUser ↓ Role ↓ Permission For example:\nUser: Alice ↓ Role: admin ↓ Permissions: user:list user:create user:update user:delete Another user:\nUser: Bob ↓ Role: viewer ↓ Permissions: user:list So the Router no longer cares about:\nIs Alice an administrator? What role does Bob have? How many roles does Carol belong to? The Router only cares about:\nWhich Permission does this endpoint require?\nFor example:\nsystem:user:delete And then lets the unified permission system answer:\nDoes the current user have this permission? 6. How Does miniagent Obtain User Permissions? In miniagent\u0026rsquo;s AsyncMenuDatabase, user permissions are not hard-coded in Routers, but obtained through relationship queries:\nUser ↓ Role ↓ Menu / Permission Corresponding to this relational query in the code:\nselect(Menu.name) .select_from(User) .join(User.roles) .join(Role.menus) .where( User.id == user_id, Menu.is_active.is_(True) ) Then converted into:\nset(result.scalars().all()) that is, a permission set. This already forms the classic RBAC pattern:\nUser ↓ Role ↓ Resource Code Business endpoints don\u0026rsquo;t need to know how the role table, the user-role mapping table, or the role-menu mapping table are actually queried.\n7. Even Super Admins Must Not Lead to if is_admin Everywhere Super administrators are another spot where permission systems easily get messy. The most dangerous approach is:\nif user.username == \u0026#34;admin\u0026#34;: return True or:\nif user.role == \u0026#34;superadmin\u0026#34;: ... scattered across dozens of business files.\nminiagent instead uses the unified:\nSUPER_PERMISSION If the user belongs to the super role:\nreturn {SUPER_PERMISSION} Regular users get their actual permission set returned. In the end, all permission checks converge into:\nif SUPER_PERMISSION in perms or required in perms: return Otherwise access is denied.\nThis way the super-admin rule has exactly one authoritative implementation, instead of:\nif is_super: popping up all over the project.\n8. Routers Should \u0026ldquo;Declare Permissions\u0026rdquo;, Not \u0026ldquo;Implement Permissions\u0026rdquo; This is the single most important sentence in the whole design.\nThe wrong Router:\n@router.delete(\u0026#34;/users/{user_id}\u0026#34;) async def delete_user( user_id: int, request: Request ): token = request.headers.get(\u0026#34;Authorization\u0026#34;) username = jwt_auth.verify_token(token) user = await user_db.get_user(username) permissions = await menu_db.get_user_resource_codes( user.id ) if ( \u0026#34;system:user:delete\u0026#34; not in permissions and \u0026#34;*\u0026#34; not in permissions ): raise HTTPException(status_code=403) return await user_service.delete(user_id) Here the Router is simultaneously doing:\nParse the header Verify the JWT Look up the user Look up permissions Check super admin Enforce the permission check Execute the business The Router is no longer a Router.\nThe more reasonable version:\n@router.delete(\u0026#34;/users/{user_id}\u0026#34;) async def delete_user( user_id: int, current_user_id: int = Depends( Permission(\u0026#34;system:user:delete\u0026#34;) ) ): return await user_service.delete(user_id) Now the business layer expresses only:\nDeleting a user requires system:user:delete.\nAs for:\nHow is the token extracted? How is the JWT verified? How is the user looked up? How are permissions queried? How is caching handled? How is a super admin determined? How is the 403 raised? All of that is the unified auth system\u0026rsquo;s responsibility.\n9. The Permission Design in miniagent AuthPermission.Permission in miniagent does exactly this.\nThe core flow is very clear:\nasync def __call__( self, request: Request, credentials: HTTPAuthorizationCredentials = Depends( _bearer_scheme ), ) -\u0026gt; int: auth = request.app.state.container.auth user_id = await auth.resolve_user_id( credentials.credentials ) await auth.check( user_id, self._code ) return user_id That is:\nBearer Token ↓ resolve_user_id() ↓ JWT verification ↓ User identity ↓ check() ↓ Permission set ↓ required permission ↓ Allow / Deny This logic is executed by the unified dependency, not implemented by each business Router itself.\n10. Permission Queries Also Need Caching After unifying authentication, a practical problem appears.\nIf every API request executes:\nJWT verification ↓ Query User ↓ Query Role ↓ Query Permission this adds database load under high concurrency, so the permission set is a great fit for caching.\nIn miniagent:\nCACHE_TTL_SECONDS = 3600.0 that is, one hour of caching by default.\nThe lookup logic is:\nflowchart TB A[Request permissions] --\u003e B{Cache exists?} B --\u003e|Yes| C[Return directly] B --\u003e|No| D[Query database] D --\u003e E[Write to cache] E --\u003e C In the code:\ncached = self._cache.mget_ttl( [self._cache_key(user_id)] )[0] if cached is not None: return self._decode(cached) return await self._load_permissions(user_id) When permissions change, the cache is proactively invalidated via:\ninvalidate(user_id) So unified authentication not only makes the code cleaner, it also leaves a unified entry point for performance optimization.\n11. Authentication, Authorization, and Business Should Form Clear Boundaries The overall architecture can ultimately be understood as:\nminiagent\u0026rsquo;s JWTAuth handles unified token issuance and verification.\nminiagent\u0026rsquo;s AuthPermission handles resolving the user from the token, checking user status, loading and caching permissions, and performing the final permission decision.\nminiagent\u0026rsquo;s permission data is read through the User → Role → Menu relationships; the super role is uniformly mapped to SUPER_PERMISSION, rather than repeatedly checking admin status in business code.\nIn short:\nflowchart TB A[Request] --\u003e B[Bearer Token] B --\u003e C[\"JWTAuthToken issuanceSignature verificationExpiration check\"] C --\u003e D[\"AuthPermissionUser resolutionUser statusPermission loadingPermission cachingPermission check\"] D --\u003e E[Permission] E --\u003e F[Router] F --\u003e G[Service] G --\u003e H[Repository] The biggest value here is not saving a few lines of code.\nIt is the crispness of the boundaries:\nJWTAuth owns the \u0026#34;token\u0026#34; AuthPermission owns \u0026#34;authentication + authorization\u0026#34; Permission owns \u0026#34;declaring endpoint permissions\u0026#34; Router owns \u0026#34;request orchestration\u0026#34; Service owns \u0026#34;business logic\u0026#34; As long as the AI respects these boundaries, it won\u0026rsquo;t easily scatter permission logic around.\n3. What Are the Benefits of Unified Authentication? 1. Security Rules Have Exactly One Implementation Suppose the JWT algorithm changes later.\nIf the project has 30 copies of:\njwt.decode(...) you have to review 30 places.\nIf everything is centralized in:\nJWTAuth.verify_token() you change exactly one place.\n2. Permission Logic Is Genuinely Consistent All endpoints uniformly pass through:\nresolve_user_id() ↓ get_permissions() ↓ check() So:\nHow disabled users are handled How super admins are handled How missing permissions are handled How 403 is returned How caching is handled is consistent across the board.\n3. Routers Become Easy to Understand Seeing:\nPermission(\u0026#34;system:user:delete\u0026#34;) a developer immediately knows:\nThis endpoint requires the delete-user permission.\nNo need to read 20 lines of ifs to figure out who can actually access it.\n4. Permissions Can Be Centrally Audited Once all authorization flows through the unified:\nAuthPermission.check() adding any of the following later:\nPermission-denied logging Security auditing Access statistics Anomalous behavior analysis only requires touching one entry point.\n5. It Suits AI Programming Better What AI fears most is:\nThe project has five ways of doing it, but nobody tells it which one to use. Unified authentication effectively tells the AI:\nDo not design your own permission system. Need a token: go to JWTAuth. Need the current user: go to CurrentUser. Need permissions: go to Permission. Need a permission check: go to AuthPermission. The AI\u0026rsquo;s freedom is constrained to exactly the right places.\n4. Write the Architecture Rules Down Clearly for the AI You can add the following directly to your project-level AI Rules:\n## Authentication and Authorization Authentication and permission control must be implemented in a unified way. ### JWT - It is forbidden to create or verify JWTs inside Routers, Services, or Repositories; - All JWT issuance and verification must reuse the project\u0026#39;s existing JWTAuth; - It is forbidden to design another payload, algorithm, or expiration policy. ### Permissions - The permission model uses RBAC; - Hard-coded role checks in business code are forbidden; - Scattered permission if-checks are forbidden; - Routers declare permissions via the existing Permission / AuthPermission; - Services must not re-implement endpoint-level permission checks; - The super-admin bypass rule must be implemented centrally. ### Responsibilities JWTAuth: owns the Token. AuthPermission: owns authentication, permission loading, caching, and permission checks. Router: only declares which permission is required. Service: only handles business logic. Putting It into Prompts: Don\u0026rsquo;t Ask the AI to \u0026ldquo;Add Some Auth\u0026rdquo; Bad prompt:\nAdd admin permissions to this endpoint.\nThe problem with this sentence is:\nthe AI has no idea how \u0026ldquo;admin permissions\u0026rdquo; should actually be implemented in your project.\nSo it will very likely write:\nif user.role != \u0026#34;admin\u0026#34;: A more sensible prompt:\nPlease add permission control to this endpoint. It must follow the current project\u0026#39;s unified auth architecture: 1. Do not manually parse the Authorization Header in the Router; 2. Do not call jwt.decode directly; 3. Do not check role == \u0026#34;admin\u0026#34; in the Router or Service; 4. Do not add scattered permission if-checks; 5. Reuse the existing AuthPermission / Permission; 6. The Router only declares the required permission; 7. Continue using the existing JWTAuth for JWT; 8. Continue using the existing SUPER_PERMISSION for super admins; 9. Do not change the existing RBAC data model; 10. Do not re-implement the existing permission caching logic. With constraints like these, the AI no longer needs to \u0026ldquo;design a permission system\u0026rdquo;.\nIt only needs to:\nPlug into the existing permission system.\nWhen Developing New Features, Ask the AI One Question First From now on, when asking the AI to add an endpoint, you can first require:\nBefore modifying any code, please check: 1. Whether the project already has a unified JWT implementation; 2. Whether the project already has an authentication dependency; 3. Whether the project already has a permission model; 4. Whether the project already has a way to declare permissions; 5. Which existing mechanism the current endpoint should reuse. If an existing mechanism exists, re-implementation is forbidden. This sentence is crucial:\nIf an existing mechanism exists, re-implementation is forbidden.\nBecause one of the biggest hidden risks of AI programming is:\nThe old code already solved it once, and the AI very diligently solves it a second time. Summary The core of unified authentication and RBAC is not:\nwriting more security checks.\nQuite the opposite — it demands:\nDon\u0026rsquo;t let security checks scatter across business code.\nA healthy permission architecture should be crystal clear:\nJWT handles identity credentials Authentication answers \u0026#34;who are you\u0026#34; RBAC answers \u0026#34;which permissions you hold\u0026#34; Authorization answers \u0026#34;may you do this\u0026#34; Router declares which permission is required Service executes the actual business And in the age of AI programming, one more hard rule should be added:\nThe AI may call the permission system, but it is not allowed to reinvent the permission system.\nDon\u0026rsquo;t let the project end up as:\nif user.role == \u0026#34;admin\u0026#34;: if user.is_super: if permission in permissions: if username == \u0026#34;root\u0026#34;: scattered everywhere. What we really want to see is:\nPermission(\u0026#34;system:user:delete\u0026#34;) with all the complex authentication and authorization machinery working behind a unified architecture.\nThat is RBAC\u0026rsquo;s greatest value for AI programming:\nTurning \u0026ldquo;who can do what\u0026rdquo; into a unified rule set, instead of letting the AI guess it anew in every business file.\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0111/","summary":"\u003cp\u003eIn AI programming, there is one category of code that easily \u0026ldquo;grows and grows messier\u0026rdquo;:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003erole\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;admin\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eHTTPException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estatus_code\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e403\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSwitch to another endpoint, and the AI writes:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;user:delete\u0026#34;\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"ow\"\u003ein\u003c/span\u003e \u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003epermissions\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eForbiddenError\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYet another endpoint:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent_user\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e \u003cspan class=\"o\"\u003e!=\u003c/span\u003e \u003cspan class=\"n\"\u003eowner_id\u003c/span\u003e \u003cspan class=\"ow\"\u003eand\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrent_user\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eis_admin\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eHTTPException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003estatus_code\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003e\u003cspan class=\"mi\"\u003e403\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEach snippet looks defensible on its own. But once dozens of variations of these \u003ccode\u003eif\u003c/code\u003es appear across the project, the permission system has effectively gone out of control.\u003c/p\u003e\n\u003cp\u003eThe mature solution is not \u0026ldquo;make the AI write its if-statements more carefully\u0026rdquo;, but to establish a unified:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eAuthentication + permission model + permission enforcement mechanism\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe most common permission model here is \u003cstrong\u003eRBAC (Role-Based Access Control)\u003c/strong\u003e.\u003c/p\u003e\n\u003chr\u003e","title":"Unified Authentication and Authorization: Standardizing AI Token Issuance and Verification Logic"},{"content":"In the age of AI programming, there is one kind of code that gets copied around with abandon:\nlogger.info(\u0026#34;Start processing request\u0026#34;) if not token: raise UnauthorizedError() start = time.time() result = await service.do_something() logger.info(f\u0026#34;Execution time: {time.time() - start}\u0026#34;) return result At first there is only one endpoint, and it looks harmless enough.\nBut once the project grows to dozens or even hundreds of endpoints, things quickly become:\nUser endpoints ├─ Authentication ├─ Logging ├─ Parameter validation ├─ Business logic └─ Instrumentation Agent endpoints ├─ Authentication ├─ Logging ├─ Parameter validation ├─ Business logic └─ Instrumentation Knowledge base endpoints ├─ Authentication ├─ Logging ├─ Parameter validation ├─ Business logic └─ Instrumentation ... The only truly different part is the few lines of business logic in the middle. As for the rest, every time the AI writes another endpoint, it dutifully copies it all over again.\nThis is exactly the problem that AOP (Aspect-Oriented Programming) set out to solve.\n📌 Technology Card AOP / Aspect-Oriented Programming\nA software design philosophy that extracts logic shared by many business modules out of the specific business code and handles it in one place.\nThis shared logic is commonly referred to as:\nCross-Cutting Concerns\nTypical examples include: logging, authentication, authorization, auditing, performance metrics, tracing, caching, transactions, instrumentation, and exception handling.\nThe core of AOP is not any particular framework or special syntax, but a very simple architectural principle:\nBusiness code handles business; shared rules handle shared rules.\n💡 An Intuitive Way to Understand It: AOP Is Like Airport Security Imagine an airport with many boarding gates.\nThe Beijing flight has one gate:\nSecurity check ↓ ID check ↓ Register information ↓ Board the plane The Shanghai flight is the same:\nSecurity check ↓ ID check ↓ Register information ↓ Board the plane \u0026hellip;\nIf we followed the approach many AIs take when writing code, it would probably turn into:\nEvery gate gets its own security checkpoint equipment.\nObviously unreasonable. What happens in the real world is:\nflowchart TB A[Passenger request] --\u003e B[Unified entrance] subgraph CROSS[\"Unified security (cross-cutting logic)\"] B --\u003e C[Identity check] C --\u003e D[Security screening] D --\u003e E[Information registration] end E --\u003e F[Boarding area] F --\u003e G[Beijing flight] F --\u003e H[Shanghai flight] F --\u003e I[Guangzhou flight] All passengers go through the unified security checkpoint first, and then proceed to their own gates.\nSoftware is no different.\nWithout cross-cutting governance:\nRouter A ├─ Logging ├─ Token validation ├─ Permission check ├─ Business code └─ Performance metrics Router B ├─ Logging ├─ Token validation ├─ Permission check ├─ Business code └─ Performance metrics After extraction:\nflowchart TB A[Request] --\u003e B[\"MiddlewareLogging / Request IDPerformance / Audit\"] B --\u003e C[\"AuthenticationIdentity / Permissions\"] C --\u003e D[Router] D --\u003e E[Service] E --\u003e F[Repository] The Router can then refocus on a single job:\nHandling business.\n💡 Don\u0026rsquo;t Mistake AOP for \u0026ldquo;You Must Use an AOP Framework\u0026rdquo; When many developers first encounter AOP, they think of Spring AOP in Java.\nFor example:\n@Before @After @Around But that is just one implementation of AOP. What truly matters about AOP is:\nIdentifying cross-cutting concerns and extracting them from business code.\nIn a Python + FastAPI project, you can perfectly well use:\nMiddleware Dependency Injection Decorator ContextVar Unified exception handling to achieve the same architectural goal. For example, miniagent currently takes exactly this approach.\nThe core directories of the repository already clearly place general-purpose capabilities in app/core, which contains:\ncore/ ├── audit_context.py ├── deps.py ├── logger_config.py ├── security/ │ ├── auth_permission.py │ ├── jwt_auth.py │ └── ... └── service_container.py rather than scattering authentication, logging, and audit logic across every business Router.\nThis is, in fact, a very Python/FastAPI-flavored realization of AOP thinking.\n1. Architectural Rules: Where Should Cross-Cutting Logic Live? For a FastAPI project, you can give the AI a very simple decision standard.\nGlobal request-level logic → Middleware Middleware is suitable for concerns that every HTTP request may involve.\nFor example:\nRequest logging Request ID Endpoint latency Global auditing Tracing Unified headers The basic structure:\n@app.middleware(\u0026#34;http\u0026#34;) async def middleware(request, call_next): # Before the request ... response = await call_next(request) # After the request ... return response The idea behind it is:\nBefore ↓ Request → Middleware → Router ↑ After That is, the classic AOP pattern of:\nUniformly inserting shared behavior before and after business logic executes.\n2. The Real Implementation in miniagent The diagram below fairly completely illustrates the AOP approach of miniagent:\nIn the current main.py, after a request enters the unified logging middleware, a request_id is created, the start time is recorded, and an audit context is established — and only then is the actual Router invoked. After the request completes, the status code, latency, and audit results are logged uniformly, and X-Request-ID is returned to the client.\nAt the same time, AuthPermission centrally handles JWT verification, user resolution, user status checks, permission caching, and permission decisions, and writes identity information into the audit context.\nAnd audit_context.py uses a ContextVar to hold the current request\u0026rsquo;s:\nrequest_id method path ip_address user_id username change_count so that this information propagates along the async call chain without being passed down layer by layer as function parameters.\nTogether, these three components form:\nMiddleware + Dependency / Permission + ContextVar a three-layer cross-cutting system.\nThis is not the \u0026ldquo;heavyweight AOP\u0026rdquo; traditionally implemented via complex proxy mechanisms.\nInstead, it fits the character of a FastAPI project better:\nImplementing AOP thinking with the framework\u0026rsquo;s native capabilities.\n1. Logging Is Not Written by Every Endpoint In miniagent\u0026rsquo;s main.py there is a unified HTTP request logging middleware:\n@app.middleware(\u0026#34;http\u0026#34;) async def log_requests(request: Request, call_next): \u0026#34;\u0026#34;\u0026#34;Record all HTTP requests and inject request_id into every log line.\u0026#34;\u0026#34;\u0026#34; request_id = str(uuid4()) request.state.request_id = request_id start_time = time.time() with logger.contextualize(request_id=request_id): logger.info( f\u0026#34;📥 {request.method} {request.url.path}\u0026#34; ) response = await call_next(request) process_time = time.time() - start_time logger.info( f\u0026#34;📤 {request.method} {request.url.path} \u0026#34; f\u0026#34;- {response.status_code} ({process_time:.3f}s)\u0026#34; ) response.headers[\u0026#34;X-Process-Time\u0026#34;] = str(process_time) response.headers[\u0026#34;X-Request-ID\u0026#34;] = request_id return response This is an excerpt based on the current repository code; the actual implementation also handles exceptions, auditing, and login logging.\nThe change it brings is significant. Before, you might have:\n@router.get(\u0026#34;/users\u0026#34;) async def list_users(): logger.info(\u0026#34;GET /users start\u0026#34;) start = time.time() result = await user_service.list_users() logger.info( f\u0026#34;GET /users finished: {time.time() - start}\u0026#34; ) return result Now the Router can simply be:\n@router.get(\u0026#34;/users\u0026#34;) async def list_users(): return await user_service.list_users() Because questions like:\nWho made the request? Which endpoint was accessed? When did it start? When did it end? What was the status code? How long did it take? What is the Request ID? no longer belong to the Router — the Middleware takes care of them uniformly.\nThis is what it means to:\nCut cross-cutting logic out of business code.\n2. Identity and Permissions → Dependency Not all cross-cutting logic is suitable for Middleware.\nFor example:\n/user/profile only requires being logged in.\nWhereas:\n/admin/users may require the system:user:list permission.\nOr, for instance:\n/admin/users/{id} may require the system:user:delete permission when deleting a user.\nClearly, you can\u0026rsquo;t simply have one global Middleware decide every business permission.\nThat\u0026rsquo;s where FastAPI\u0026rsquo;s:\nDependency Injection\ncomes in, i.e.:\nDepends(...) 3. Authentication: Not Every Router Re-Parses JWT miniagent implements a unified AuthPermission, responsible for:\nJWT verification ↓ Resolve the user ↓ Check user status ↓ Load the permission set ↓ Permission check JWT (JSON Web Token) is a common token format for authentication.\nRather than having every endpoint write its own:\ntoken = request.headers.get(\u0026#34;Authorization\u0026#34;) username = verify_token(token) user = await find_user(username) permissions = await load_permissions(user.id) if permission not in permissions: raise HTTPException(...) miniagent encapsulates this whole flow inside AuthPermission.\nFor example, the core permission-check logic is very clean:\nasync def check( self, user_id: int, required: str, ) -\u0026gt; None: perms = await self.get_permissions(user_id) if SUPER_PERMISSION in perms or required in perms: return raise_forbidden( \u0026#34;auth.permission_denied\u0026#34;, required=required, ) The permission data itself is also cached, avoiding a fresh database read on every request.\nThis way, the business layer only needs to express:\nWhat permission this endpoint requires.\nInstead of re-implementing:\nHow the permission system actually works.\n4. Declare Rules, Don\u0026rsquo;t Repeatedly Implement Them This is a very important point of AOP thinking.\nGood business code should be as close as possible to:\n@router.delete(\u0026#34;/users/{user_id}\u0026#34;) async def delete_user(...): return await user_service.delete(user_id) while declaring, via a Dependency, Decorator, or other unified mechanism:\nRequires login Requires system:user:delete permission instead of:\n@router.delete(\u0026#34;/users/{user_id}\u0026#34;) async def delete_user(...): # Parse Authorization ... # Verify JWT ... # Look up user ... # Look up permissions ... # Check super admin ... # Write access log ... # Record start time ... # ===== The real business finally begins ===== await user_service.delete(user_id) # ===== Business ends ===== # Write audit log ... # Write timing log ... return ... The biggest danger of the latter is not the code length.\nIt is:\nAI is exceptionally good at copying this kind of code.\nAnd then copying it dozens of times.\n5. Wrapping Permissions as a Callable Dependency AuthPermission also contains a design well worth borrowing for AI projects:\nclass Permission: def __init__( self, permission_code: str, ) -\u0026gt; None: self._code = permission_code async def __call__( self, request: Request, credentials = Depends(_bearer_scheme), ) -\u0026gt; int: auth = request.app.state.container.auth user_id = await auth.resolve_user_id( credentials.credentials ) await auth.check( user_id, self._code ) return user_id This way, the permission component itself becomes a dependency object FastAPI can recognize.\nThe whole relationship can be understood as:\nRouter │ │ Declares the permission ↓ Permission(\u0026#34;system:user:delete\u0026#34;) │ ↓ AuthPermission │ ├── Verify JWT ├── Resolve User ├── Load Permission └── Check Permission The Router doesn\u0026rsquo;t need to understand:\nHow is a JWT parsed? How is the user looked up? Where is the permission cache? How is a super admin determined? It is only responsible for expressing:\nI want system:user:delete This is a fundamentally important architectural idea:\nThe business layer declares intent; the infrastructure layer implements the mechanism.\n6. ContextVar Solves Cross-Layer Context Passing Logging and auditing face another troublesome problem.\nSuppose the call chain is:\nHTTP Request ↓ Router ↓ Service ↓ Repository ↓ Database If every layer needs:\nrequest_id user_id username The most straightforward approach might be:\nservice.run( request_id=request_id, user_id=user_id, ... ) And then:\nrepository.save( request_id=request_id, user_id=user_id, ... ) This quickly pollutes every function signature.\nminiagent uses the following for its audit context:\nContextVar (Context Variable)\nThe code defines:\n_audit_context: ContextVar[ Optional[AuditRequestContext] ] = ContextVar( \u0026#34;audit_request_context\u0026#34;, default=None ) The context is established when the request enters:\nbegin_audit_context(...) And when the request ends:\nreset_audit_context(...) After authentication succeeds, it calls:\nset_audit_user( user.id, user.username ) to attach the user identity to the audit context of the current request.\nSo the whole flow becomes:\nHTTP Request ↓ Middleware │ ├── request_id ├── method ├── path └── ip ↓ Audit Context ↓ AuthPermission │ ├── user_id └── username ↓ Audit Context ↓ Service / Repository This is another kind of cross-cutting capability:\nThe request context propagates along the entire async call chain without polluting business function parameters.\n3. What Are the Actual Benefits of AOP? 1. Less Duplicate Code The most direct change:\n100 endpoints no longer means:\n100 copies of logging code 100 copies of auth code 100 copies of timing code 100 copies of audit code Shared rules are implemented exactly once.\n2. Changing a Rule Means Changing It in One Place Suppose the logs later need to add:\nrequest_id client_ip user_id latency If logging is scattered across 200 endpoints:\nCost of change ≈ 200 edits With Middleware:\nCost of change ≈ 1 edit This is the problem architecture truly solves.\n3. Business Code Becomes Easier to Read A good Router should let you see at a glance:\nWhat request it accepts Which Service it calls What result it returns instead of first wading through dozens of lines of:\ntoken logger permission cache metrics audit before finding the actual business.\n4. Shared Rules Stay Consistent If you let the AI write authentication 50 times, you may end up with 50 subtly different versions.\nFor example:\nif permission not in permissions: Another one:\nif required_permission not in permissions: Another forgets the super admin:\nif required not in permissions: Another forgets to check whether the user is disabled.\nOne of the values of AOP is:\nGiving every shared rule a single authoritative implementation.\n4. Establish Explicit AOP Architecture Rules for the AI You can write the following directly into your Project Rules:\n## Cross-Cutting Concerns Logging, authentication, authorization, auditing, metrics, tracing, and exception handling are cross-cutting concerns. It is forbidden to implement this logic repeatedly in Routers or business Services. Unified rules: - Global HTTP request logic uses Middleware; - Authentication and permission checks prefer FastAPI Depends; - Use Decorators only when dependency injection is unsuitable; - Request-scoped context uses ContextVar; - Exception and response conversion uses global exception handlers; Routers are only responsible for request orchestration; Services are only responsible for business logic; Repositories are only responsible for data access. These few lines of rules constrain the AI far more effectively than:\nPlease write high-quality code Don\u0026rsquo;t Tell the AI to \u0026ldquo;Add Some Auth\u0026rdquo; If you simply say:\nAdd permission control to this endpoint.\nThe AI will very likely invent a permission system on the spot. A better prompt is:\nPlease add permission control to this endpoint. Requirements: 1. Do not parse JWT in the Router; 2. Do not re-implement permission checks; 3. Use the project\u0026#39;s existing AuthPermission; 4. Perform the permission check via FastAPI Depends or the existing Permission mechanism; 5. The Router only declares the required permission; 6. Reuse the existing auth singleton in the ServiceContainer; 7. Do not alter the existing unified exception system. This effectively tells the AI:\nDo not invent mechanisms; only use the mechanisms the project already has.\nLogging Requirements Should Be Given to the AI the Same Way Bad prompt:\nAdd access logging to every endpoint. The AI will very likely modify dozens of Routers directly:\nlogger.info(...) Better prompt:\nWe need to add HTTP request access logging. First determine whether this feature is a cross-cutting concern. Requirements: 1. Do not modify Routers one by one; 2. Implement it uniformly with FastAPI Middleware; 3. Generate a request_id for every request; 4. Record method, path, status_code, and latency; 5. Return X-Request-ID in the response; 6. The logging context should propagate automatically to other modules within the current request; 7. Do not pollute business parameters in Services and Repositories. This sentence:\nFirst determine whether this is a cross-cutting concern\nis well worth adding to your AI project rules.\nDon\u0026rsquo;t Let Instrumentation Turn Business Code into a \u0026ldquo;Christmas Tree\u0026rdquo; Many systems later add:\nUser clicks Agent invocation counts LLM token consumption Endpoint latency Knowledge base hit rate Tool success rate If all of it is written inline in the business code:\nmetrics.inc(\u0026#34;agent_call\u0026#34;) logger.info(...) tracer.start(...) audit.record(...) result = await agent.run() metrics.observe(...) logger.info(...) tracer.end(...) in the end, the actual business logic shrinks to:\nresult = await agent.run() Code like this still runs, but it shows the cross-cutting logic has intruded too deeply into the business.\nThe healthier direction looks like:\nMetrics Logging Tracing Audit │ │ Cutting across horizontally ↓ ──────────────────── Router → Service ──────────────────── rather than:\nRouter ↓ Metrics ↓ Logging ↓ Tracing ↓ Auth ↓ Audit ↓ Business Summary AOP is not mysterious. The essential problem it solves is:\nExtracting the code that is \u0026ldquo;needed everywhere, but doesn\u0026rsquo;t truly belong to any single business\u0026rdquo; out of the business logic.\nLogging, authentication, auditing, instrumentation, performance metrics, and tracing are all classic cross-cutting concerns.\nFor a FastAPI project, there is no need to introduce a complex framework just to chase the name \u0026ldquo;AOP\u0026rdquo;.\nA very practical combination is enough:\nMiddleware + Dependency Injection + Decorator + ContextVar + Global Exception Handler miniagent currently follows exactly this direction.\nAnd in AI programming, we should go one step further and write it down as an explicit project rule:\nWhen you discover duplicated cross-cutting logic, do not keep copying. Stop first and judge whether it should be extracted into Middleware, a Dependency, a Decorator, or unified infrastructure.\nUltimately, what we really want from AI is not:\nEvery file looks pretty on its own. but:\nThe whole project, taken together, still looks like it was designed by one person. That is the true value of architecture in the age of AI programming.\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0110/","summary":"\u003cp\u003eIn the age of AI programming, there is one kind of code that gets copied around with abandon:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Start processing request\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eif\u003c/span\u003e \u003cspan class=\"ow\"\u003enot\u003c/span\u003e \u003cspan class=\"n\"\u003etoken\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003eraise\u003c/span\u003e \u003cspan class=\"n\"\u003eUnauthorizedError\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003estart\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eservice\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003edo_something\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"sa\"\u003ef\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Execution time: \u003c/span\u003e\u003cspan class=\"si\"\u003e{\u003c/span\u003e\u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003etime\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e \u003cspan class=\"n\"\u003estart\u003c/span\u003e\u003cspan class=\"si\"\u003e}\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAt first there is only one endpoint, and it looks harmless enough.\u003c/p\u003e\n\u003cp\u003eBut once the project grows to dozens or even hundreds of endpoints, things quickly become:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUser endpoints\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Authentication\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Logging\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Parameter validation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Business logic\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  └─ Instrumentation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eAgent endpoints\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Authentication\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Logging\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Parameter validation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Business logic\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  └─ Instrumentation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eKnowledge base endpoints\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Authentication\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Logging\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Parameter validation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ├─ Business logic\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  └─ Instrumentation\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ...\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe only truly different part is the few lines of business logic in the middle. As for the rest, every time the AI writes another endpoint, it dutifully copies it all over again.\u003c/p\u003e\n\u003cp\u003eThis is exactly the problem that \u003cstrong\u003eAOP (Aspect-Oriented Programming)\u003c/strong\u003e set out to solve.\u003c/p\u003e\n\u003chr\u003e","title":"Aspect-Oriented Programming: Guiding AI to Extract Cross-Cutting Logic So Logging, Auth, and Instrumentation Are Never Written Twice"},{"content":" Define the API rules clearly first, then let AI write the code.\nIn AI-assisted development, interface design is an area that easily \u0026ldquo;looks like it works, but gets increasingly messy over time.\u0026rdquo;\nAsk AI to write a few APIs for you, and it might quickly generate:\n/getUser /createUser /update_user /delete-user /userList They all work functionally. But as the project continues to grow, you easily end up with:\nSame resource, different naming Same action, different HTTP methods Same error, different response structures Same pagination, different field names Eventually, frontend developers, backend developers, testers, and even AI itself start guessing:\n\u0026ldquo;How exactly should this API be called?\u0026rdquo;\nWhat\u0026rsquo;s truly missing here is not coding ability, but:\nA unified API contract.\n📌 Technical Profile RESTful API (Representational State Transfer Application Programming Interface)\nIs a common network interface design style.\nIt emphasizes designing URLs around \u0026ldquo;resources\u0026rdquo; and expressing operations through HTTP methods.\nFor example:\nGET /agents POST /agents GET /agents/12 PUT /agents/12 DELETE /agents/12 Here:\nGET: Read a resource POST: Create a resource PUT: Fully update a resource PATCH: Partially modify a resource DELETE: Delete a resource And:\nContract First\nRefers to:\nDefining what the interface looks like first, then implementing the internal logic.\nIn other words, first determine:\nWhat is the URL? What is the HTTP method? What are the input fields? What are the output fields? How are errors returned? What are the status codes? Then developers or AI implement the code according to this contract.\nA Simple Analogy: An API Is Like a Restaurant Menu You can think of a software interface as a restaurant\u0026rsquo;s menu.\nThe menu states:\nKung Pao Chicken Price: 38 yuan Spiciness: Medium Portion: 1 serving The customer doesn\u0026rsquo;t need to know:\nWho the chef is What brand the wok is How the kitchen prep works How much oil goes into the chicken The customer only needs to know:\nI order from the menu, and you serve me what\u0026rsquo;s on the menu.\nAPIs work the same way.\nThe frontend shouldn\u0026rsquo;t need to care about:\nHow the database is queried How the Service is implemented How the Repository is written It only needs to know:\nRequest URL Request method Parameter format Response result Error format So an API contract is like:\nThe menu a software system publishes to the outside world.\nIf the menu changed every day:\nYesterday it was \u0026#34;Kung Pao Chicken\u0026#34; Today it\u0026#39;s \u0026#34;Spicy Diced Chicken\u0026#34; Tomorrow it becomes \u0026#34;Chicken Combo A\u0026#34; Customers would be driven crazy — and the same goes for APIs.\n1. The Core of RESTful: URLs Express \u0026ldquo;Resources,\u0026rdquo; HTTP Methods Express \u0026ldquo;Actions\u0026rdquo; This is the most important concept to understand about RESTful.\nMany projects start out writing things like this:\n/getAgent /createAgent /updateAgent /deleteAgent This approach essentially:\nPuts actions into the URL.\nWhereas RESTful recommends:\nGET /agents POST /agents PUT /agents/{agent_id} DELETE /agents/{agent_id} Because:\n/agents Represents the resource:\nThe Agent collection.\nAs for \u0026ldquo;read, create, update, delete\u0026rdquo; — that\u0026rsquo;s expressed by HTTP methods.\nSo the interface language becomes very unified:\nResource + HTTP Method Instead of:\nResource + Custom Verb + Custom Naming Convention 2. Why Is This Rule Especially Important for AI Programming? Human developers working in a project for a few years will gradually form habits.\nBut AI is different. Every time AI generates code, it is essentially reasoning from scratch:\nWhat should this API be called? Should this update use POST or PUT? What does a successful deletion return? What are the pagination fields called? Without architectural rules, AI might generate the following the first time:\nPOST /createAgent Next time it generates:\nPOST /agents/create And the time after that:\nPOST /agent All three sets of APIs work, but the project has already started to lose control.\nSo:\nRESTful isn\u0026rsquo;t about pursuing \u0026ldquo;textbook beauty\u0026rdquo; — it\u0026rsquo;s about reducing AI\u0026rsquo;s room for free improvisation.\n3. API Design Rules 3.1 Unified Resource Naming Resource names should use:\nNouns Plural form Consistent naming style For example:\n/users /agents /tools /documents /knowledge-bases Rather than:\n/userList /getAgents /toolManage /queryDocument miniagent\u0026rsquo;s current backend APIs use clear resource-oriented paths.\nFor example, the main program uniformly registers:\napp.include_router( admin_agent_router, prefix=\u0026#34;/api/v1/admin/agents\u0026#34;, tags=[\u0026#34;Admin - Agent\u0026#34;] ) app.include_router( admin_tool_router, prefix=\u0026#34;/api/v1/admin/tools\u0026#34;, tags=[\u0026#34;Admin - Tool\u0026#34;] ) app.include_router( admin_document_router, prefix=\u0026#34;/api/v1/admin/documents\u0026#34;, tags=[\u0026#34;Admin - Document\u0026#34;] ) You can see the resource naming maintains:\n/agents /tools /documents /knowledge-bases /prompts /system-settings Without stuffing actions like:\nget create delete update into the URL.\n3.2 HTTP Methods Express Actions Take miniagent\u0026rsquo;s Agent management APIs as an example.\nRead the list:\n@router.get(\u0026#34;\u0026#34;) async def list_agents(...): Read a single Agent:\n@router.get(\u0026#34;/{agent_id}\u0026#34;) async def get_agent(...): Create:\n@router.post(\u0026#34;\u0026#34;) async def create_agent(...): Update:\n@router.put(\u0026#34;/{agent_id}\u0026#34;) async def update_agent(...): Delete:\n@router.delete(\u0026#34;/{agent_id}\u0026#34;) async def delete_agent(...): Combined together:\nGET /api/v1/admin/agents GET /api/v1/admin/agents/{agent_id} POST /api/v1/admin/agents PUT /api/v1/admin/agents/{agent_id} DELETE /api/v1/admin/agents/{agent_id} Even if you\u0026rsquo;ve never seen the miniagent codebase, you can basically guess what these APIs do.\nThis is what good API design brings:\nPredictability.\n3.3 Don\u0026rsquo;t Misuse PUT and PATCH This is a place where AI gets very easily confused.\nPUT typically means:\nUpdate a resource.\nFor example:\nPUT /agents/12 Means update the Agent with ID 12.\nAnd PATCH means:\nPartial Update\nThat is, only modifying part of the resource\u0026rsquo;s state.\nminiagent has a very intuitive example:\n@router.patch(\u0026#34;/{agent_id}/toggle\u0026#34;) async def toggle_agent_active(...): Here, it\u0026rsquo;s not resubmitting the entire Agent — it\u0026rsquo;s just:\nToggling whether the Agent is active.\nSo using PATCH is more semantically appropriate.\nFor beginners, you can start by remembering:\nPUT → Update a resource PATCH → Partial modification 3.4 RESTful Doesn\u0026rsquo;t Mean \u0026ldquo;Actions Must Never Appear in URLs\u0026rdquo; Many people take RESTful to the other extreme after learning it:\nAbsolutely no actions should ever appear in URLs.\nThat\u0026rsquo;s actually unnecessary.\nSome business operations are not simple CRUD.\nCRUD stands for:\nCreate, Read, Update, Delete\nFor example:\n/agents/{id}/toggle Expresses:\nToggle the Agent\u0026rsquo;s state.\nOr in the future there might be:\n/documents/{id}/reindex /tasks/{id}/cancel /agents/{id}/run These are essentially business commands.\nThe point is not to mechanically pursue \u0026ldquo;pure REST,\u0026rdquo; but rather:\nResource-oriented operations follow a unified paradigm, and special business actions have clear and stable naming rules.\n3.5 A Contract Is More Than Just URLs Many projects believe:\nOnce the URLs are unified, the API standards are complete.\nThat\u0026rsquo;s far from enough. A complete API contract should at least include:\nRequest path HTTP method Path parameters Query parameters Request Body Response Body HTTP Status Code Error model Where:\nPath Parameter:\nPath Parameter\nFor example:\n/agents/{agent_id} Query Parameter:\nQuery Parameter\nFor example:\n/agents?page=1\u0026amp;page_size=20 Request Body:\nRequest Body\nThe data body sent by the client.\nResponse Body:\nResponse Body\nThe data body returned by the server.\nHTTP Status Code:\nHTTP Status Code\nFor example:\n200 OK 201 Created 404 Not Found 409 Conflict 500 Internal Server Error 3.6 Query Parameters Must Also Be Unified Suppose in one system you see:\n?page=1\u0026amp;pageSize=20 In another API:\n?pageIndex=1\u0026amp;limit=20 And in yet another:\n?offset=0\u0026amp;size=20 Each one works.\nBut the problem is:\nThe frontend can never remember them all.\nminiagent\u0026rsquo;s Agent list API uses:\npage: int = Query(1, ge=1) page_size: int = Query(20, ge=1, le=100) And the pagination result is defined as:\nclass PageResult(BaseModel, Generic[T]): total: int page: int page_size: int data: List[T] So the entire project can uniformly use:\npage page_size total data Instead of reinventing pagination for every module.\n3.7 API Responses Must Have a Unified \u0026ldquo;Envelope\u0026rdquo; This is a very important part of Contract First.\nThe most easily lost scenario is:\nUser API returns:\n{ \u0026#34;success\u0026#34;: true, \u0026#34;user\u0026#34;: {} } Agent API returns:\n{ \u0026#34;code\u0026#34;: 0, \u0026#34;result\u0026#34;: {} } Knowledge base API returns:\n{ \u0026#34;status\u0026#34;: \u0026#34;ok\u0026#34;, \u0026#34;data\u0026#34;: {} } The frontend has to handle each module differently.\nThis is:\nAPI contract fragmentation.\nminiagent defines a unified response model:\nclass ApiResponse(BaseModel, Generic[T]): code: int = 200 message: str = \u0026#34;success\u0026#34; data: Optional[T] = None So successful responses can be uniformly:\n{ \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: {} } When there\u0026rsquo;s no data, it can also be:\n{ \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34; } The most important value of this design isn\u0026rsquo;t saving a few lines of code.\nIt\u0026rsquo;s that:\nThe frontend only needs to learn the response protocol once.\n3.8 HTTP Status Codes and Business Response Structures Should Each Serve Their Purpose When miniagent creates an Agent:\n@router.post( \u0026#34;\u0026#34;, response_model=ApiResponse, status_code=status.HTTP_201_CREATED ) Here it uses:\n201 Created To indicate:\nThe server successfully created a new resource.\nWhile read or update operations typically return:\n200 OK If the resource doesn\u0026rsquo;t exist, the global exception handler returns:\n404 If it already exists:\n409 For general bad requests:\n400 For internal server errors:\n500 miniagent has already uniformly mapped these error types in its global exception handler.\nThis essentially establishes:\nDomain Exception ↓ Unified Exception Handler ↓ HTTP Status Code ↓ Unified ApiResponse So the business layer doesn\u0026rsquo;t have to decide on its own:\nShould I return 200? Or 404? What should the error JSON look like? 3.9 Contract First Truly Solves \u0026ldquo;Who Has the Final Say\u0026rdquo; Without a contract, the development process easily becomes:\nFrontend thinks it should be this way ↓ Backend writes it another way ↓ AI generates a third version ↓ Fix during integration testing With Contract First, it becomes:\nDefine the API first ↓ Determine the inputs ↓ Determine the outputs ↓ Determine the status codes ↓ Frontend implements ↓ Backend implements ↓ Automated testing The API becomes:\nA protocol that both frontend and backend jointly follow.\n3.10 FastAPI Is Naturally Suited for Contract First FastAPI is:\nA framework for building Web APIs based on Python type annotations.\nOne of its important features is:\nIt can directly generate API documentation from the type definitions in the code.\nFor example:\nasync def create_agent( payload: AgentCreate, ): Here:\nAgentCreate Is itself the input contract.\nAnd:\nresponse_model=ApiResponse Declares:\nThe output must conform to the ApiResponse contract.\nminiagent also enables during FastAPI initialization:\ndocs_url=\u0026#34;/docs\u0026#34; redoc_url=\u0026#34;/redoc\u0026#34; FastAPI generates API documentation based on OpenAPI.\nOpenAPI:\nOpenAPI Specification\nIs a machine-readable API description standard.\nThis way, the API contract doesn\u0026rsquo;t just exist in people\u0026rsquo;s heads — it can be directly read by:\nFrontend Testing tools API debugging tools Code generators AI 3.11 RESTful + Schema + ApiResponse Form a Complete API Standard Using RESTful alone doesn\u0026rsquo;t solve all problems.\nA truly stable API system is usually:\nRESTful URL + HTTP Method + Schema + Status Code + Unified Response + Unified Exception Where Schema can be understood as:\nData structure contract.\nFor example:\nAgentCreate AgentUpdate AgentOut Respectively defining:\nWhat fields are allowed when creating an Agent What fields are allowed when updating an Agent What fields are included when returning an Agent Rather than using a single:\ndict for everything.\n4. What Are the Benefits? After unifying API design, the most obvious change isn\u0026rsquo;t \u0026ldquo;prettier code\u0026rdquo; — it\u0026rsquo;s the reduction in cognitive overhead for the entire team.\n4.1 APIs Become Guessable Seeing:\nGET /agents/12 Without checking documentation, you basically know:\nGet Agent 12.\nSeeing:\nDELETE /agents/12 You can immediately tell:\nDelete Agent 12.\nThis is:\nPredictability through consistency.\n4.2 Reduced Frontend-Backend Communication Cost No need to discuss every time a new API is added:\nShould it be called getAgentById or queryAgent? The rules are already established.\n4.3 Easier Test Automation Once APIs are standardized:\nPOST Create GET Query PUT Update DELETE Delete Automated testing tools can more easily batch-generate tests.\n4.4 More Stable Documentation FastAPI can generate API documentation directly from:\nRouter Schema Response Model The API definition itself is part of the documentation.\n4.5 Better Suited for AI Programming This point is especially important.\nAI doesn\u0026rsquo;t need to guess:\nWhat should the API be called? How should the response be wrapped? How should errors be handled? How should pagination be designed? It just needs to follow the rules.\n5. Write API Standards Directly into AI Project Rules Architecture design alone isn\u0026rsquo;t enough. If you want AI to comply long-term, you need to write these constraints into the project rules.\nFor example:\n## API Design Rules 1. Follow RESTful resource-oriented API design. 2. URLs must use nouns, not CRUD verbs. 3. Use plural forms for resource names where appropriate. 4. Use GET method to read resources. 5. Use POST method to create resources. 6. Use PUT method to update resources. 7. Use PATCH method for partial state changes. 8. Use DELETE method to delete data. 9. All regular API responses must use the ApiResponse type. 10. Paginated responses must use the PageResult type. 11. Request and response data must use Pydantic schemas. 12. Routers must not return arbitrary dictionary structures. 13. Routers only handle HTTP-related concerns. 14. Business logic should be placed in Service classes. 15. Business errors must be raised as domain exceptions. 16. Do not repeat exception handling within individual routers. 17. Reuse existing API patterns before introducing new endpoints. Simply put:\nURLs only describe resources ↓ HTTP methods express actions ↓ Schema defines inputs and outputs ↓ ApiResponse unifies responses ↓ Domain exceptions are handled uniformly ↓ Routers don\u0026#39;t contain business logic This way, every time AI generates a new API, it already knows:\nIt cannot redesign a new API style.\n6. How to Write Prompts That Actually Work Don\u0026rsquo;t just tell AI:\nPlease help me write Agent CRUD APIs. CRUD stands for:\nCreate, Read, Update, Delete\nA better prompt is:\nPlease implement management APIs for the Agent resource. You must follow the project\u0026#39;s existing API contract: - RESTful resource-oriented URLs - Reuse the /api/v1/admin/agents path structure - GET for querying - POST for creating - PUT for updating - DELETE for deleting - Use PATCH for partial state modifications - Use existing Pydantic schemas for request parameters - Use ApiResponse uniformly for return values - Use PageResult for pagination - Routers only handle HTTP protocol and dependency injection - All business logic goes into AgentService - Use the existing domain exception system for business errors - Do not redefine exception return formats in Routers - Before implementing, refer to existing Agent, Tool, and KnowledgeBase API styles in the project At this point, AI\u0026rsquo;s task shifts from:\n\u0026ldquo;Help me design and write an API.\u0026rdquo;\nTo:\n\u0026ldquo;Build according to the existing contract.\u0026rdquo;\nThe stability of the generated code is completely different between the two.\n7. Positive Outcome: The API Paradigm Already Established in miniagent Combining the actual code from miniagent, you can see a relatively clear API chain.\nIn simple terms:\nHTTP Request │ ▼ FastAPI Router │ ├── Path / Query ├── Pydantic Schema ├── Permission └── HTTP Method │ ▼ Service │ ▼ Domain / Repository / Runtime │ ▼ Result │ ▼ ApiResponse │ ▼ HTTP Response If an exception occurs during business processing:\nService │ ▼ Domain Exception │ ▼ Global Exception Handler │ ├── 400 ├── 404 ├── 409 └── 500 │ ▼ ApiResponse All of these parts have real implementations in miniagent:\nRouters use GET / POST / PUT / PATCH / DELETE to describe operations. Inputs and outputs are modeled through Pydantic Schemas. All regular responses uniformly use ApiResponse. Pagination results uniformly use PageResult. Business logic is moved from Routers down to Services. Domain exceptions are uniformly converted to HTTP status codes and response structures by the global exception handler. FastAPI automatically provides /docs and /redoc API documentation endpoints. What ultimately emerges is not a few isolated APIs, but a set of:\nPredictable, reusable, extensible API design paradigms that AI can continuously follow.\nFinal Thoughts Many people first encounter RESTful and think it\u0026rsquo;s just:\nGET for querying POST for creating PUT for updating DELETE for deleting But once you get into engineering practice, you realize:\nThe core value of RESTful is actually a unified language.\nAnd Contract First further solves:\nFixing this language in place before code implementation begins.\nFor traditional development teams, this reduces communication costs. For AI programming, its value is even greater.\nBecause without an API contract:\nAI ↓ Guesses URLs on its own ↓ Guesses HTTP methods on its own ↓ Guesses parameters on its own ↓ Guesses response structures on its own ↓ Guesses error handling on its own ↓ Project gradually develops multiple API styles After establishing API standards:\nAI ↓ Reads API Rules ↓ Follows RESTful ↓ Reuses Schema ↓ Reuses ApiResponse ↓ Calls Service ↓ Unified exception handling AI is no longer responsible for \u0026ldquo;inventing APIs\u0026rdquo; — it only handles:\nCompleting implementations according to existing API contracts.\nThis is one of the most important ideas in architecturally constraining AI programming:\nUnify the rules first, then expand generation capabilities.\nWhen API paths, HTTP methods, inputs and outputs, status codes, and exception protocols are all stabilized, the faster AI writes, the less likely the project is to lose control.\nThat is the true engineering value of RESTful and Contract First.\nOpen Source Code github gitee 🪐 Wishing you good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0109/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eDefine the API rules clearly first, then let AI write the code.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIn AI-assisted development, interface design is an area that easily \u0026ldquo;looks like it works, but gets increasingly messy over time.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003eAsk AI to write a few APIs for you, and it might quickly generate:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/getUser\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/createUser\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/update_user\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/delete-user\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e/userList\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThey all work functionally. But as the project continues to grow, you easily end up with:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eSame resource, different naming\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eSame action, different HTTP methods\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eSame error, different response structures\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eSame pagination, different field names\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eEventually, frontend developers, backend developers, testers, and even AI itself start guessing:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;How exactly should this API be called?\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eWhat\u0026rsquo;s truly missing here is not coding ability, but:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eA unified API contract.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e","title":"RESTful and Contract First: Standardizing Interface Definitions and Unifying API Design Paradigms"},{"content":" Standardize log formats, log levels, and trace instrumentation so that AI stops logging chaotically everywhere.\nAs AI-assisted programming becomes increasingly common, an easily overlooked problem is emerging:\nAI is very good at writing logs, but not necessarily good at writing \u0026ldquo;useful logs.\u0026rdquo;\nAsk AI to implement a feature, and it will likely generate something like:\nlogger.info(\u0026#34;start\u0026#34;) logger.info(\u0026#34;processing...\u0026#34;) logger.info(\u0026#34;data loaded\u0026#34;) logger.error(\u0026#34;failed\u0026#34;) Individually, these seem fine. But as the project grows larger, you\u0026rsquo;ll find your logs turning into something like this:\nstart processing... loading data... done request failed retry... success When something actually breaks, it becomes very difficult to answer a few of the most basic questions:\nWhich request caused the error? Which module caused the error? Which step caused the error? \u0026hellip; The problem is rarely \u0026ldquo;too few logs.\u0026rdquo; It\u0026rsquo;s usually:\nThere are plenty of logs, but no system behind them.\nTherefore, just like exception handling, dependency injection, and interface contracts, logging should also be part of the project architecture — not left to the free rein of developers or AI.\n📌 Technical Profile Logging Architecture\nRefers to the standardized management of a system\u0026rsquo;s operational records through unified logging components, formats, levels, contextual fields, and trace identifiers.\nWhat it solves is not merely:\n\u0026ldquo;How do I print a line of text?\u0026rdquo;\nBut rather:\nAfter a problem occurs in the system, can we quickly reconstruct what happened?\nA reasonably complete logging system typically includes:\nUnified Log Entry Point ↓ Unified Log Format ↓ Log Level Standards ↓ Request Context ↓ Trace Identifier (request_id) ↓ File / JSON / Log Platform ↓ Search, Troubleshooting, Audit, Performance Analysis Here, request_id can be understood as:\nThe ID card number of a single request.\nAs long as the entire call chain carries this number, you can reassemble logs scattered across different modules.\n1. Why Can\u0026rsquo;t We Let AI Log Freely? Suppose we ask AI to implement three modules.\nAI might write them separately as:\nlogger.info(\u0026#34;user created\u0026#34;) logger.info(\u0026#34;Create agent success\u0026#34;) logger.info(f\u0026#34;knowledge base {kb_id} loaded\u0026#34;) And even:\nprint(\u0026#34;start\u0026#34;) Each one works on its own.\nBut when the entire project is assembled, several typical problems emerge.\n1.1 Inconsistent Formats Some logs write:\ncreate user success Some write:\nUser created successfully And some even write:\nok!!! Machines can barely perform stable analysis on these.\n1.2 Chaotic Log Levels For example, AI easily writes:\nlogger.error(\u0026#34;User not found\u0026#34;) But \u0026ldquo;user not found\u0026rdquo; is often just a normal business outcome and should not be logged as a system error.\nConversely:\nlogger.info(f\u0026#34;Database connection failed: {e}\u0026#34;) A database connection failure is logged as ordinary information.\nEventually it becomes:\nERROR ERROR ERROR ERROR ERROR And genuinely important errors get drowned out.\n1.3 Logs from the Same Request Can\u0026rsquo;t Be Correlated A single chat request might pass through:\nHTTP API ↓ ChatService ↓ AgentRunner ↓ Tool ↓ Knowledge Base ↓ LLM Each layer has its own logs.\nWithout a unified trace identifier, all you see is dozens of unrelated messages.\n2. Logging Rules 2.1 Unified Log Entry Point The most important first step in a logging system is not designing the format. It is:\nThe entire project has only one standard way to use logging.\nTake miniagent as an example. The project centralizes core logging configuration in:\nbackend/app/core/logger_config.py And provides a unified method:\ndef get_logger(name: str = None): if name: return logger.bind(name=name) return logger Business modules uniformly use:\nfrom app.core.logger_config import get_logger logger = get_logger(__name__) Instead of having some places doing:\nimport logging Other places doing:\nfrom loguru import logger And still other places doing:\nprint(...) miniagent currently uses Loguru as its logging library and manages console, file, error, and debug logs through a unified configuration.\nThis approach has one critically important value:\nAI doesn\u0026rsquo;t need to redesign the logging system every time — it only needs to follow the entry point already defined by the project.\n2.2 Unified Log Format A good log format should at least answer:\nWhen? What level? Which request? Which module? Which function? What happened? miniagent\u0026rsquo;s current console log format is roughly:\nTime | Level | request_id | Module:Function:Line | Message The actual configuration looks something like:\nformat=( \u0026#34;{time:YYYY-MM-DD HH:mm:ss.SSS} | \u0026#34; \u0026#34;{level: \u0026lt;8} | \u0026#34; \u0026#34;{extra[request_id]} | \u0026#34; \u0026#34;{extra[name]}:{function}:{line} | \u0026#34; \u0026#34;{message}\u0026#34; ) The resulting log looks something like:\n2026-08-12 18:21:31.426 | INFO | 2b91c7... | app.services.chat:send_message:126 | Agent execution started This single log entry already contains several core dimensions:\nTime ↓ Log Level ↓ request_id ↓ Module ↓ Function ↓ Code Line ↓ Event So troubleshooting no longer relies on \u0026ldquo;guessing.\u0026rdquo;\n2.3 Clear Log Levels Common log levels include:\nDEBUG — Debug Information DEBUG stands for Debug.\nPrimarily used during development to observe internal state, for example:\nlogger.debug(f\u0026#34;Retrieved {len(chunks)} chunks\u0026#34;) Suitable for recording:\nIntermediate variables Number of retrieval results Routing decisions Internal execution steps Model parameters Usually not output in large quantities in production environments.\nINFO — Normal Operation Information INFO stands for Information.\nIndicates that the system is performing important actions normally:\nlogger.info(\u0026#34;Agent execution started\u0026#34;) For example:\nApplication startup User login Task started Agent invocation completed Knowledge base loaded Request completed WARNING — Warnings WARNING indicates:\nThe system can still continue running, but a noteworthy issue has occurred.\nFor example:\nlogger.warning(\u0026#34;Knowledge base returned no result\u0026#34;) Or business exceptions:\nlogger.warning(f\u0026#34;NotFoundError: {exc}\u0026#34;) miniagent\u0026rsquo;s global exception handler currently logs predictable business exceptions like NotFoundError and AlreadyExistsError as WARNING, rather than treating them as system crashes.\nThis is a very important logging philosophy:\nA business failure does not equal a system fault.\nERROR — System Errors ERROR indicates:\nA certain function can no longer be completed normally.\nFor example:\nlogger.error(\u0026#34;Database initialization failed\u0026#34;) Suitable for:\nDatabase connection failure LLM call failure File read failure Critical service unavailable CRITICAL — Severe Failures CRITICAL stands for Critical.\nUsed for:\nSystem cannot start Core database corruption Critical configuration missing Core infrastructure unavailable These logs typically mean:\nThe system may no longer be able to provide service.\n3. Don\u0026rsquo;t \u0026ldquo;Log Everything\u0026rdquo; There is another important principle in logging systems:\nNot every step executed is worth becoming a log entry.\nFor example:\nlogger.info(\u0026#34;enter function\u0026#34;) logger.info(\u0026#34;get user\u0026#34;) logger.info(\u0026#34;check user\u0026#34;) logger.info(\u0026#34;start processing\u0026#34;) logger.info(\u0026#34;processing...\u0026#34;) logger.info(\u0026#34;return result\u0026#34;) The primary effect of such logs is usually just:\nGenerating noise.\nA better approach is to log \u0026ldquo;events.\u0026rdquo;\nFor example:\nlogger.info( f\u0026#34;Agent execution started: agent_id={agent_id}\u0026#34; ) And:\nlogger.info( f\u0026#34;Agent execution completed: agent_id={agent_id}, \u0026#34; f\u0026#34;duration={duration:.3f}s\u0026#34; ) Logs should primarily record:\nState changes Key decisions External calls Exceptions Performance metrics Security events Business audit events Rather than:\nI reached line 17. 4. The Truly Critical Step: Adding Trace Identifiers As systems grow complex, a single HTTP request might pass through dozens of functions.\nFor example, in miniagent:\nPOST /chat │ ▼ Chat API │ ▼ ChatService │ ▼ AgentRunner │ ├── LLM │ ├── Knowledge Base │ └── Web Search If each module logs independently, it\u0026rsquo;s hard to know which logs belong to the same request.\nThe solution is:\nrequest_id That is:\nRequest Identifier\nminiagent generates one for each request in the HTTP middleware:\nrequest_id = str(uuid4()) Where UUID stands for:\nUniversally Unique Identifier\nThen it writes:\nrequest.state.request_id = request_id Suppose this request gets:\nrequest_id = 742fd2b1... Now all subsequent logs carry:\n742fd2b1... And so:\n742fd2b1 | HTTP request received 742fd2b1 | Agent started 742fd2b1 | KB retrieval started 742fd2b1 | KB returned 6 chunks 742fd2b1 | LLM started 742fd2b1 | Agent completed 742fd2b1 | HTTP 200 A complete call chain forms instantly.\n5. miniagent Logging System: From a Single Request to a Fully Traceable Chain Below is the overall relationship of miniagent\u0026rsquo;s current logging architecture:\nflowchart TB A[\"HTTP RequestClient Request\"] --\u003e B[\"FastAPI MiddlewareRequest Logging Middleware\"] B --\u003e C[\"Generate request_idGenerate Unique Request Identifier\"] C --\u003e D[\"Logging ContextLog Context\"] C --\u003e E[\"Audit ContextAudit Context\"] D --\u003e F[\"Application CodeBusiness Code\"] F --\u003e F1[\"API / Service\"] F --\u003e F2[\"AgentRunner\"] F --\u003e F3[\"Knowledge Base / RAG\"] F --\u003e F4[\"LLM / Tool\"] F1 --\u003e G[\"get_logger(__name__)\"] F2 --\u003e G F3 --\u003e G F4 --\u003e G H[\"Third-party LibrariesThird-party Components\"] --\u003e H1[\"Uvicorn\"] H --\u003e H2[\"SQLAlchemy\"] H --\u003e H3[\"ChromaDB\"] H1 --\u003e I[\"Python logging\"] H2 --\u003e I H3 --\u003e I I --\u003e J[\"InterceptHandlerStandard Log Bridge\"] G --\u003e K[\"LoguruUnified Log Center\"] J --\u003e K D -. \"request_id auto-injected\" .-\u003e K K --\u003e L[\"ConsoleConsole\"] K --\u003e M[\"miniagent_YYYY-MM-DD.logINFO and above\"] K --\u003e N[\"error.logERROR and above\"] K --\u003e O[\"debug.logDEBUG / Development\"] K --\u003e P[\"Structured JSON LogJSON Structured Log\"] E --\u003e Q[\"Audit Log DBDatabase Audit Record\"] C -. \"same request_id\" .-\u003e Q B --\u003e R[\"HTTP Response\"] R --\u003e S[\"X-Request-IDReturn Trace Identifier\"] R --\u003e T[\"X-Process-TimeReturn Request Duration\"] The most important things in this diagram are actually three main lines.\nThe first line is:\nHTTP Request ↓ Middleware ↓ request_id ↓ Logging Context ↓ Business Code ↓ Loguru ↓ Console / File / JSON The second line is:\nUvicorn / SQLAlchemy / ChromaDB ↓ Python logging ↓ InterceptHandler ↓ Loguru The third line is:\nrequest_id / \\ ↓ ↓ Application Logs Audit Context ↓ Audit Log DB In other words, miniagent doesn\u0026rsquo;t simply \u0026ldquo;unify log printing.\u0026rdquo; It places:\nApplication logs Third-party logs Request traces Audit records Performance timing into a single correlated system.\n5.1 How to Automatically Propagate request_id If every function passes it like this:\nservice.run(request_id=request_id) That certainly works.\nBut as call depth increases, the code becomes very ugly.\nminiagent uses Loguru\u0026rsquo;s contextualization mechanism:\nwith logger.contextualize(request_id=request_id): response = await call_next(request) Logs executed within this request scope automatically receive the corresponding request_id.\nSo business code can still simply write:\nlogger.info(\u0026#34;Agent started\u0026#34;) But the output automatically becomes:\n742fd2b1 | Agent started This is what we call:\nContext Logging\nIts core idea is:\nBusiness Code does not repeatedly pass request_id ↓ Infrastructure Layer auto-injects context This is a very worthwhile architectural boundary to enforce with AI.\n5.2 Returning request_id to the Frontend The trace identifier isn\u0026rsquo;t just for the server\u0026rsquo;s own use.\nminiagent also:\nresponse.headers[\u0026#34;X-Request-ID\u0026#34;] = request_id HTTP Header stands for:\nHypertext Transfer Protocol Header\nSo if a frontend user reports:\nThe chat API returned an error. Developers no longer need to ask:\nAround what time? Which user? What did they send? The frontend just needs to provide:\nX-Request-ID: 742fd2b1... The server searches directly for:\n742fd2b1 And can reconstruct the entire request process.\nThis is logging evolving from:\n\u0026ldquo;Printing text\u0026rdquo;\nto:\nA fault tracing system.\n5.3 Trace Logs Can Also Connect with Audit Logs miniagent goes a step further.\nThe audit context also stores:\n@dataclass class AuditRequestContext: request_id: str method: str path: str ip_address: Optional[str] = None user_id: Optional[int] = None username: Optional[str] = None And when the HTTP request creates an audit context, it directly reuses the same:\nrequest_id=request_id So:\nApplication Logs │ │ request_id ▼ 742fd2b1 Database Audit Records │ │ request_id ▼ 742fd2b1 The two systems are now connected.\nYou can know not only:\nWhat went wrong with the program But also:\nWho When Through which endpoint Performed what operation What the final result was This is especially important for backend management systems.\n5.4 Third-Party Library Logs Must Also Be Consolidated Real-world projects have another common problem.\nYour own code uses Loguru:\nlogger.info(...) But many third-party libraries use Python\u0026rsquo;s standard logging module:\nlogging.getLogger(...) For example:\nUvicorn SQLAlchemy ChromaDB If left unhandled, you\u0026rsquo;ll see:\n2026-08-12 | INFO | miniagent ... INFO: uvicorn request ... sqlalchemy.engine INFO ... Log formatting becomes fragmented again.\nminiagent implements:\nclass InterceptHandler(logging.Handler): To forward all Python standard logging output to Loguru.\nThen:\nlogging.basicConfig( handlers=[InterceptHandler()], level=0, force=True ) And specifically intercepts:\nuvicorn uvicorn.error uvicorn.access So the final result is:\nBusiness Code ──────┐ │ FastAPI ────────────┤ │ Uvicorn ────────────┤ ├──→ Loguru SQLAlchemy ─────────┤ │ ChromaDB ───────────┘ All logs use the same format, the same file strategy, and the same trace mechanism.\n5.5 Different Logs Should Go to Different Files Stuffing all logs into a single:\napp.log is simple but not suitable for long-term operation.\nminiagent currently splits logs by purpose.\nGeneral Logs miniagent_2026-08-12.log Records:\nINFO WARNING ERROR CRITICAL Rotated daily, retaining 30 days.\nError Logs error.log Records only:\nERROR CRITICAL Rotated when the file reaches 10 MB, keeping recent historical files.\nThis way, when troubleshooting production issues, you can go directly to:\nerror.log Without searching through hundreds of thousands of normal request logs for anomalies.\nDebug Logs In development mode:\ndebug.log Records more detailed:\nDEBUG information.\nThis prevents production environments from continuously generating large volumes of meaningless debug logs.\n5.6 Structured Logging: Preparing Logs for Machines Traditional logs are meant for humans:\n2026-08-12 18:20:31 | INFO | 742fd2b1 | Agent completed But if you plan to integrate with a log analysis platform in the future, a format like this is more suitable:\n{ \u0026#34;time\u0026#34;: \u0026#34;2026-08-12T18:20:31\u0026#34;, \u0026#34;level\u0026#34;: \u0026#34;INFO\u0026#34;, \u0026#34;request_id\u0026#34;: \u0026#34;742fd2b1\u0026#34;, \u0026#34;module\u0026#34;: \u0026#34;agent_runner\u0026#34;, \u0026#34;event\u0026#34;: \u0026#34;agent_completed\u0026#34;, \u0026#34;duration_ms\u0026#34;: 842 } This approach is called:\nStructured Logging\nThe most common format is JSON.\nJSON stands for:\nJavaScript Object Notation\nIts biggest advantage isn\u0026rsquo;t \u0026ldquo;looking sophisticated\u0026rdquo; — it\u0026rsquo;s that machines can easily search, filter, aggregate\u0026hellip;\nminiagent already supports generating structured log files via configuration:\nJSON_LOG_ENABLED=true And uses Loguru\u0026rsquo;s:\nserialize=True to output JSON logs.\nAfter that, it becomes much easier to integrate with:\nELK Loki Grafana Where:\nELK stands for:\nElasticsearch + Logstash + Kibana\nResponsible for log storage/search, collection/processing, and visualization respectively.\nGrafana is commonly used for:\nMonitoring metrics and log visualization.\nFor miniagent\u0026rsquo;s current stage, local files are sufficient. When deployment scale grows, a centralized log platform can be added.\nThis is what we mean by:\nDesign the architectural boundaries first, rather than piling on complex infrastructure from the start.\n6. How Should Logging Rules for AI Be Written? Once we\u0026rsquo;ve established a logging system, we should further communicate the rules to AI.\nFor example, add this to the project rules:\n## Logging Rules 1. Do not use `print()` for runtime logging. 2. Use `get_logger(__name__)` for application logging. 3. Do not create custom logging configurations within business modules. 4. Use `DEBUG` for internal diagnostic information. 5. Use `INFO` for important business events. 6. Use `WARNING` for recoverable or expected exceptional conditions. 7. Use `ERROR` for operation failures. 8. Use `logger.exception()` when a stack trace is needed. 9. Do not log passwords, tokens, API keys, or other sensitive data. 10. Do not manually generate `request_id` within business services. 11. Request-scoped logs will automatically inherit the middleware\u0026#39;s `request_id`. 12. Prefer meaningful events over process messages such as \u0026#34;start\u0026#34;, \u0026#34;processing\u0026#34;, or \u0026#34;done\u0026#34;. Translated into architectural requirements, this means:\nNo print ↓ Unified Logger ↓ Unified Levels ↓ Unified Format ↓ Auto-attach request_id ↓ No Sensitive Information ↓ Only Log Valuable Events From now on, when AI writes new features, it should no longer freely improvise the logging system.\n7. Logging Caveats 7.1 Logs Also Need \u0026ldquo;Prohibited Actions\u0026rdquo; Defined Log standards don\u0026rsquo;t just specify:\nWhat should be logged.\nThey must also clearly state:\nWhat must absolutely never be logged.\nFor example:\nlogger.info(f\u0026#34;password={password}\u0026#34;) Should absolutely never appear.\nThis also includes:\nPasswords JWTs API Keys Access Tokens Refresh Tokens Database passwords Full cookies ID numbers and other sensitive information Where JWT stands for:\nJSON Web Token\nAnd API stands for:\nApplication Programming Interface\nLog files are often retained for long periods and may be read by:\nDevelopers Operations staff Log servers Monitoring systems Therefore:\nLogs are not temporary debug windows — they are persistent data.\n7.2 The Most Common Logging Mistakes AI Makes When having AI write code in the future, you can focus on checking the following situations.\n❌ Mistake 1: Overusing INFO logger.info(\u0026#34;enter method\u0026#34;) logger.info(\u0026#34;checking param\u0026#34;) logger.info(\u0026#34;query database\u0026#34;) logger.info(\u0026#34;return result\u0026#34;) Process noise should be reduced.\n❌ Mistake 2: Only Printing the Exception String except Exception as e: logger.error(str(e)) This often loses the:\nStack Trace\nIt\u0026rsquo;s better to use:\nexcept Exception: logger.exception(\u0026#34;Agent execution failed\u0026#34;) ❌ Mistake 3: Duplicate Exception Logging For example:\nlogger.error(f\u0026#34;Failed: {exc}\u0026#34;) logger.exception(exc) Unless there\u0026rsquo;s a specific purpose, this typically creates duplicate logs.\n❌ Mistake 4: All Business Exceptions as ERROR For example:\nlogger.error(\u0026#34;User not found\u0026#34;) In many cases, the more appropriate choice is:\nlogger.warning(\u0026#34;User not found\u0026#34;) ❌ Mistake 5: Business Layer Generating Its Own request_id request_id = uuid4() This breaks the entire call chain.\nYou should reuse the request context already created at the entry layer.\n8. What a Logging System Truly Constrains Is AI\u0026rsquo;s \u0026ldquo;Freedom\u0026rdquo; The biggest problem with AI writing code is usually not an inability to implement features. Quite the opposite:\nIt implements features too easily.\nWithout project rules, every time AI writes a module, it might invent:\nA new logging approach A new exception handling approach A new return structure A new utility class A new naming convention The code works individually, but the project as a whole becomes increasingly chaotic.\nThis is where the value of a logging system lies:\nWithout a Logging Architecture: AI ↓ Freely decides format ↓ Freely decides level ↓ Freely decides fields ↓ Freely decides whether to print ↓ Logs gradually spiral out of control After establishing rules:\nAI ↓ get_logger() ↓ Logging Rules ↓ Unified Level ↓ Unified request_id ↓ Unified Output AI no longer needs to \u0026ldquo;design logging.\u0026rdquo;\nIt just needs to:\nWrite business code within existing boundaries.\nFinal Thoughts A mature software project should not rely on developers \u0026ldquo;remembering how to log.\u0026rdquo;\nNor should it expect AI to automatically guess each time:\nWhich logs are important What level they should be What context they should carry Where they should be stored These things should be determined at the architecture level in advance.\nThe goal of a logging system is also not:\nTo make the system print more content.\nBut rather:\nTo reconstruct what actually happened in the system using as few — but sufficiently critical — logs as possible.\nFor AI programming, this constraint is especially important.\nBecause what truly needs to be standardized is not:\nHow to write logger.info() But rather:\nWhen AI is allowed to write logs, what logs to write, and how those logs enter the system\u0026rsquo;s observable trace chain.\nOnce log format, levels, context, and trace identifiers are all fixed, AI output is no longer a pile of scattered debug messages.\nInstead, it becomes a set of:\nSearchable, correlatable, troubleshootable, auditable, and analyzable engineering logs.\nThat is the real problem a logging architecture should solve.\nOpen Source Code github gitee 🪐 Wishing you good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0108/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eStandardize log formats, log levels, and trace instrumentation so that AI stops logging chaotically everywhere.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAs AI-assisted programming becomes increasingly common, an easily overlooked problem is emerging:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAI is very good at writing logs, but not necessarily good at writing \u0026ldquo;useful logs.\u0026rdquo;\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eAsk AI to implement a feature, and it will likely generate something like:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;start\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;processing...\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003einfo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;data loaded\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003elogger\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eerror\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;failed\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIndividually, these seem fine. But as the project grows larger, you\u0026rsquo;ll find your logs turning into something like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003estart\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eprocessing...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eloading data...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003edone\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003erequest failed\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eretry...\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esuccess\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen something actually breaks, it becomes very difficult to answer a few of the most basic questions:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eWhich request caused the error?\u003c/li\u003e\n\u003cli\u003eWhich module caused the error?\u003c/li\u003e\n\u003cli\u003eWhich step caused the error?\u003c/li\u003e\n\u003cli\u003e\u0026hellip;\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe problem is rarely \u0026ldquo;too few logs.\u0026rdquo; It\u0026rsquo;s usually:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eThere are plenty of logs, but no system behind them.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eTherefore, just like exception handling, dependency injection, and interface contracts, \u003cstrong\u003elogging should also be part of the project architecture — not left to the free rein of developers or AI.\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e","title":"Logging Architecture: Making Every Log Line AI Writes Valuable"},{"content":"When AI writes code, it loves the \u0026ldquo;fastest way to make it run\u0026rdquo; approach, for example:\ndb = Database() cache = RedisCache() mailer = EmailService() service = UserService() Create wherever needed.\nIn the short term it\u0026rsquo;s convenient, but as the project grows larger, you end up with:\nDatabase instances created everywhere Cache clients created everywhere Configuration scattered Services instantiating each other Dependencies impossible to replace during testing The most obvious problem with this kind of code is:\nObjects hardcode their own dependencies.\nAnd DI (Dependency Injection) is exactly what solves this problem.\n📌 Technical Card DI / Dependency Injection\nWhen a class or function needs an object, it doesn\u0026rsquo;t create it itself — instead, the already-prepared object is \u0026ldquo;injected\u0026rdquo; from the outside.\nFor example:\nNot recommended:\nclass UserService: def __init__(self): self.db = UserDatabase() Recommended:\nclass UserService: def __init__(self, db): self.db = db There\u0026rsquo;s only one difference:\nUserService no longer decides how the database object is created.\nIt is only responsible for:\nUsing the database.\n💡 One-Sentence Understanding You can think of DI as a restaurant.\nA chef needs the following to cook:\nIngredients Pots Gas Seasonings Normally, the restaurant prepares these things in advance.\nWhen the chef comes to work, they simply use them, rather than:\nEvery time a dish is cooked ↓ The chef goes to buy a pot ↓ Opens a gas account themselves ↓ Finds a supplier to buy ingredients Software is the same.\nWhat a business class should truly care about is:\n\u0026ldquo;What capabilities do I need to use?\u0026rdquo;\nRather than:\n\u0026ldquo;How should this capability be created?\u0026rdquo;\nSo the core of DI can be summarized as:\nObjects are responsible for using dependencies; the system is responsible for creating them.\nThis is especially important for AI programming. Because without rules, AI can easily instantiate the objects it needs directly anywhere, just to complete the current task.\n1. Anti-Pattern: AI\u0026rsquo;s \u0026ldquo;Free Rein\u0026rdquo; Suppose we tell the AI:\nImplement an AgentService that queries Agents and clears the cache after an Agent is modified.\nWithout architectural constraints, the AI would likely write:\nclass AgentService: def __init__(self): self.db = AgentDatabase() self.cache = RedisCache() async def get_agent(self, agent_id: int): return await self.db.get_agent(agent_id) async def update_agent(self, agent_id: int, data): agent = await self.db.update_agent( agent_id, data ) await self.cache.delete( f\u0026#34;agent:{agent_id}\u0026#34; ) return agent At first glance it looks perfectly reasonable, but problems have already appeared\u0026hellip;\nProblem 1: The Database Is Hardcoded self.db = AgentDatabase() This means:\nAgentService can only ever use this AgentDatabase.\nLater, if you want to:\nSwitch database implementations Use a Mock Database for testing Add a database proxy Change the connection pool You must modify AgentService.\nProblem 2: The Cache Is Also Hardcoded self.cache = RedisCache() Later, if the project wants to switch from:\nRedis To:\nIn-memory cache Other cache services Fake Cache for testing The business class must also be modified accordingly.\nProblem 3: Connections May Be Recreated Everywhere If the AI writes in many Services:\nDatabase() Redis() LLMClient() It can lead to:\nConnection pools recreated HTTP Clients recreated Model clients recreated Cache instances recreated Wasting system resources.\nProblem 4: Testing Becomes Very Difficult Suppose you now want to test:\nAgentService.update_agent() We don\u0026rsquo;t actually want to:\nConnect to SQLite Connect to Redis Initialize the entire system We just want to give it a fake database, but the object has already hardcoded it internally, making it very hard to replace during testing.\n2. Architectural Rule: Don\u0026rsquo;t Let Business Objects Create Their Own Dependencies The most important rule of DI is actually very simple:\nWhoever uses a dependency should not be responsible for creating it.\nThe creation work should be centralized in the system entry point, a container, or the framework\u0026rsquo;s dependency management mechanism.\nA simple structure could be:\nApplication Startup ↓ Create Database ↓ Create Cache ↓ Create Repository ↓ Create Service ↓ Put into DI Container ↓ Business code retrieves as needed Here:\nDI Container\nStands for:\nDependency Injection Container.\nYou can understand it as:\nA \u0026ldquo;central warehouse\u0026rdquo; that uniformly manages object creation and relationships.\nRule 1: Business Classes Should Not Proactively Instantiate Infrastructure For example, don\u0026rsquo;t do this:\nclass AgentService: def __init__(self): self.db = AgentDatabase() Instead:\nclass AgentService: def __init__(self, db): self.db = db This way:\nAgentService Only knows:\nI have a db I can use.\nAs for this db:\nWhether it\u0026#39;s SQLite Whether it\u0026#39;s PostgreSQL Whether it\u0026#39;s a Mock None of those are its concern.\nRule 2: Shared Resources Should Be Centrally Created For example:\nDatabase Engine Session Factory HTTP Client LLM Client Cache Registry Vector Store These objects generally should not be recreated for every request.\nA more reasonable approach is:\nApplication Startup ↓ Create once ↓ Reuse uniformly This is both DI and resource lifecycle management.\nRule 3: Service Dependencies Come from the Constructor or Container For example:\nclass UserService: def __init__(self, user_db): self.user_db = user_db Or:\nclass AgentService: def __init__(self, container): self._agent_db = container.agent_db self._cache = container.cache The key is not the form, but:\nDependencies come from outside, rather than being temporarily created internally.\nRule 4: APIs Should Not Create Services Themselves Don\u0026rsquo;t do this:\n@router.get(\u0026#34;/agents\u0026#34;) async def list_agents(): service = AgentService() Instead:\n@router.get(\u0026#34;/agents\u0026#34;) async def list_agents( service = Depends(get_service) ): ... Here, Depends is provided by FastAPI:\nA Dependency Injection mechanism.\nIt\u0026rsquo;s responsible for preparing dependencies before executing the endpoint.\nRule 5: Never Bypass the Container for Convenience This is especially important for AI.\nFor example, if the project already has:\ncontainer.agent_service But the AI, for convenience, writes:\nservice = AgentService(container) It might technically work, but it bypasses unified lifecycle management. So it should be made clear:\nObjects already managed by the container must be reused; arbitrary re-instantiation is not allowed.\n3. What Are the Benefits of Doing This? DI is often misunderstood by newcomers as:\n\u0026ldquo;Just a different way of passing parameters.\u0026rdquo;\nIt\u0026rsquo;s far more than that.\n1. Easier to Replace Implementations Suppose currently:\nAgentService ↓ SQLite Repository Later you need to switch to:\nAgentService ↓ PostgreSQL Repository If dependencies are injected:\nAgentService itself may not need to be modified at all.\nThis is:\nLow coupling.\nWhich means:\nA module should not be tightly bound to a specific implementation.\n2. Easier to Test In production:\nAgentService ↓ Real Agent Database During testing:\nAgentService ↓ Fake Agent Database For example:\nfake_db = FakeAgentDatabase() service = AgentService( db=fake_db ) Testing doesn\u0026rsquo;t require actually starting a database.\nThis is one of DI\u0026rsquo;s greatest contributions to:\nTestability\n3. Easier Unified Management of System Resources For example, a database Engine:\nCreate once ↓ Shared by Repositories Rather than:\nUserService → One Engine AgentService → Another Engine ToolService → Yet another Engine This is especially important for:\nDatabase connection pools LLM Clients HTTP Clients Vector databases Unified lifecycle management is crucial.\n4. Simpler Configuration Switching For example, the development environment uses:\nLocal LLM The production environment uses:\nCloud LLM If business code directly does:\nclient = OpenAIClient(...) It becomes bound to a specific implementation.\nWith DI, it can become:\nDevelopment environment ↓ LocalLLMClient Production environment ↓ CloudLLMClient Business logic only receives:\nLLM Client Without having to decide which one it is.\n5. AI Is Less Likely to Secretly Create New Infrastructure This is a particularly valuable point in AI programming.\nWithout DI rules, when AI encounters:\n\u0026ldquo;A database is needed here.\u0026rdquo;\nIt easily does:\ndb = Database(...) When it encounters:\n\u0026ldquo;A cache is needed here.\u0026rdquo;\nIt again does:\ncache = Redis(...) Eventually the entire project ends up with many duplicate objects.\nWith DI, the AI\u0026rsquo;s first reaction should become:\nDoes this dependency already exist in the project?\nInstead of:\nHow do I create a new one?\n4. Putting Prompts into Practice: Tell AI the DI Rules Just writing:\n\u0026ldquo;Use dependency injection.\u0026rdquo;\nIs still not specific enough. You can write it directly into the project rules:\n## Dependency Injection Rules This project uses Dependency Injection (DI). Rules: - Business classes must not create internal infrastructure dependencies. - Do not directly instantiate databases, repositories, caches, LLM clients, HTTP clients, vector stores, or services inside business methods. - Reuse dependencies managed by the application ServiceContainer. - Shared infrastructure resources must be centrally created and managed. - Services should receive required dependencies through constructor injection or the existing application container. - FastAPI routes must use the project\u0026#39;s existing dependency resolution mechanism. - Do not directly instantiate Service classes inside API routes. - Before creating a new object, check whether an equivalent instance already exists in the ServiceContainer. - Respect the lifecycle of container-managed singletons or shared resources. - Dependencies must be replaceable during testing whenever possible. Then specific tasks can be written like this:\nAdd new business capability to the Agent. Strictly follow the project\u0026#39;s Dependency Injection rules: 1. Do not create database instances inside Routers or Services; 2. Do not directly instantiate existing Services; 3. Do not recreate shared resources such as caches, LLM Clients, Vector Stores, etc.; 4. Prioritize obtaining existing dependencies from the ServiceContainer; 5. FastAPI APIs should use the existing Depends / get_service approach to obtain Services; 6. If a new shared dependency is genuinely needed, it should be created and managed centrally in the ServiceContainer; 7. Business classes are only responsible for using dependencies, not for deciding how dependencies are constructed. Before implementing, first check: app/core/service_container.py Related Services Related Routers Existing Repository / Runtime implementations. This will noticeably change the AI\u0026rsquo;s coding habits. It will no longer ask:\n\u0026ldquo;How do I instantiate one?\u0026rdquo;\nBut will first ask:\n\u0026ldquo;Where has the project already placed it?\u0026rdquo;\n5. Positive Output: How Does miniagent Implement DI? miniagent currently has a very clearly defined core class:\nbackend/app/core/service_container.py The file\u0026rsquo;s own description says:\n# Application-level service container, # Implement Dependency Injection. Which means:\nApplication-level Service Container, for implementing dependency injection.\n1. Database Engine Is Only Centrally Created in the Container ServiceContainer first uniformly creates:\nself.engine = create_async_engine( database_url, echo=False, future=True, ) self.session_factory = async_sessionmaker( bind=self.engine, ... ) This means:\nService A Service B Service C Don\u0026rsquo;t each need to:\ncreate_async_engine(...) Instead, they share the database infrastructure managed by the container.\n2. Repositories Are Also Uniformly Created The container then creates:\nself.user_db = AsyncUserDatabase( self.engine, self.session_factory ) self.agent_db = AsyncAgentDatabase( self.engine, self.session_factory ) self.tool_db = AsyncToolDatabase( self.engine, self.session_factory ) As well as knowledge base, Document, Chat, Role, and other data access objects.\nForming:\nServiceContainer │ ├── engine ├── session_factory │ ├── user_db ├── agent_db ├── tool_db ├── kb_db └── ... The source of all objects is very clear.\n3. Services Continue to Be Uniformly Created by the Container For example:\nself.agent_service = AgentService(self) self.llm_service = LLMService(self) self.user_service = UserService(self) self.tool_service = ToolService(self) self.kb_service = KnowledgeBaseService(self) So the overall relationship becomes:\nServiceContainer │ ├── Repository ├── Runtime ├── Cache ├── Registry └── Service That is:\nWhere objects are created is decided by the container.\n4. Example: AgentService Doesn\u0026rsquo;t Create Its Own Database Looking at the real one:\nbackend/app/services/admin/agent.py AgentService\u0026rsquo;s constructor is:\ndef __init__( self, container: ServiceContainer, ) -\u0026gt; None: self._agent_db = container.agent_db self._user_agent_relation_db = ( container.user_agent_relation_db ) self._agent_tool_relation_db = ( container.agent_tool_relation_db ) self._tool_db = container.tool_db self._cache = container.object_cache_invalidator There\u0026rsquo;s no:\nAsyncAgentDatabase(...) And no:\nCacheRegistry() Instead:\nContainer has already prepared everything ↓ AgentService retrieves and uses This is dependency injection.\n5. Example: FastAPI Router Also Doesn\u0026rsquo;t Create Its Own Service Let\u0026rsquo;s look at the Agent API.\nIt defines:\ndef get_service( request: Request ) -\u0026gt; AgentService: return request.app.state.container.agent_service The endpoint then:\nasync def create_agent( payload: AgentCreate, svc: AgentService = Depends(get_service), caller_id: int = Depends(_add), ): agent_out = await svc.create_agent(payload) return ApiResponse( data=agent_out ) Here:\nDepends(get_service) Is FastAPI\u0026rsquo;s dependency injection mechanism.\nYou can understand it as:\nHTTP Request ↓ FastAPI Depends ↓ get_service() ↓ ServiceContainer ↓ Already existing AgentService ↓ Router uses it The Router doesn\u0026rsquo;t have:\nsvc = AgentService(...) 6. What Kind of Dependency Relationship Is Ultimately Formed? Below is the dependency injection / DI implementation diagram of miniagent:\nIt can be simplified as:\nflowchart TD START[\"Application Startup\"] CONTAINER[\"ServiceContainerDI Container\"] DB[\"Database\"] RUNTIME[\"Runtime\"] CACHE[\"Cache\"] REPO[\"RepositoryData Access\"] REGISTRY[\"RegistryRegistration Center\"] SERVICE[\"ServiceBusiness Service\"] DEPENDS[\"FastAPI DependsDependency Injection\"] ROUTER[\"RouterRouting\"] START --\u003e CONTAINER CONTAINER --\u003e DB CONTAINER --\u003e RUNTIME CONTAINER --\u003e CACHE DB --\u003e REPO RUNTIME --\u003e REGISTRY CACHE --\u003e REGISTRY REPO --\u003e SERVICE REGISTRY --\u003e SERVICE SERVICE --\u003e DEPENDS DEPENDS --\u003e ROUTER The most important principle here is:\nDependencies are created externally and injected from outside in.\nRather than:\nRouter ↓ Creates its own Service ↓ Service creates its own Repository ↓ Repository creates its own Database 6. Why Is This Better Than Internal Instantiation? If written directly as:\nclass AgentService: def __init__(self): self._agent_db = AsyncAgentDatabase(...) self._tool_db = AsyncToolDatabase(...) self._cache = CacheInvalidator(...) AgentService would have to know:\nHow to create the Engine Where the SessionFactory comes from How to configure the Cache Registry How to construct the Repository It would bear too many responsibilities that don\u0026rsquo;t belong to it. Under the current structure:\nAgentService Is only responsible for:\nUsing these capabilities to accomplish Agent business logic.\n7. What Does This Mean for AI Programming? Suppose later you ask the AI to:\nAdd an EmailService to the system.\nWithout DI rules, it might:\nmailer = SMTPMailer( host=..., port=... ) And then repeat it in many places.\nWith DI rules, the reasonable thought process should become:\n1. Is this a shared service? ↓ 2. Should it go into the ServiceContainer? ↓ 3. Where is the email Client created? ↓ 4. Which Services need it? ↓ 5. Use it through container injection Similarly:\nRedis LLM Client Vector Store Web Search Client SQL Agent All follow the same principle.\nThus the AI\u0026rsquo;s behavior shifts from:\n\u0026ldquo;Whatever I\u0026rsquo;m missing, I\u0026rsquo;ll build on the spot.\u0026rdquo;\nTo:\n\u0026ldquo;Whatever I\u0026rsquo;m missing, I\u0026rsquo;ll first check whether the system already provides it.\u0026rdquo;\nThis is actually a very significant change.\nConclusion DI (Dependency Injection) is often explained as a very abstract design pattern.\nBut for AI programming, we can understand it in one sentence:\nDon\u0026rsquo;t let business code create the objects it depends on.\nWhy?\nBecause once AI can freely instantiate:\nDatabase Cache LLM HTTP Client Repository Service It can easily create new objects just to quickly implement the current feature. After dozens of times, the system will exhibit:\nDuplicate instances Duplicate configuration Resource waste Testing difficulties Replacement difficulties Lifecycle chaos And DI establishes a very important engineering discipline for AI:\nObject creation ↓ Centrally managed Object usage ↓ Injected as needed Thus the system becomes:\nServiceContainer Responsible for \u0026#34;who is who, how to create\u0026#34; Service Responsible for \u0026#34;how to accomplish business\u0026#34; Router Responsible for \u0026#34;how to receive requests\u0026#34; AI no longer needs to figure out everywhere:\n\u0026ldquo;How do I create a Database?\u0026rdquo;\nIt only needs to care about:\n\u0026ldquo;How should I use the Database the project has already given me?\u0026rdquo;\nSo:\nThe true value of DI is not just writing fewer new statements, but reclaiming the \u0026ldquo;right to create objects\u0026rdquo; from business code.\nFor AI programming, this is especially important.\nForbid AI from arbitrary instantiation; let dependencies be uniformly supplied by the architecture.\nOnly then can you achieve genuine:\nLow coupling, testability, replaceability, and extensibility.\nOpen Source Code github gitee 🪐 Good luck to you 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0107/","summary":"\u003cp\u003eWhen AI writes code, it loves the \u0026ldquo;fastest way to make it run\u0026rdquo; approach, for example:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003edb\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eDatabase\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ecache\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eRedisCache\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emailer\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eEmailService\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eservice\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCreate wherever needed.\u003c/p\u003e\n\u003cp\u003eIn the short term it\u0026rsquo;s convenient, but as the project grows larger, you end up with:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eDatabase instances created everywhere\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eCache clients created everywhere\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eConfiguration scattered\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eServices instantiating each other\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eDependencies impossible to replace during testing\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe most obvious problem with this kind of code is:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eObjects hardcode their own dependencies.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAnd \u003cstrong\u003eDI (Dependency Injection)\u003c/strong\u003e is exactly what solves this problem.\u003c/p\u003e\n\u003chr\u003e","title":"Dependency Injection: Forbid AI from Hardcoding Instantiation, Improve Testability and Extensibility"},{"content":"AI is very good at quickly writing API endpoints. For example, if you tell it:\n\u0026ldquo;Add an endpoint to create a user.\u0026rdquo;\nIt can probably produce working code in a few minutes. But without unified standards in the project, different endpoints will quickly end up like this:\n{ \u0026#34;success\u0026#34;: true } Another endpoint returns:\n{ \u0026#34;code\u0026#34;: 0, \u0026#34;msg\u0026#34;: \u0026#34;ok\u0026#34;, \u0026#34;result\u0026#34;: {} } When yet another endpoint encounters an error, it directly returns:\n{ \u0026#34;error\u0026#34;: \u0026#34;user not found\u0026#34; } Some places even throw raw database exceptions back to the frontend.\nEvery single endpoint \u0026ldquo;works,\u0026rdquo; but the entire system becomes increasingly hard to maintain.\nSo, there\u0026rsquo;s a very important but often overlooked category of foundational architecture in AI programming:\nUnified response values + global exception handling + unified parameter validation.\nTheir purpose is to define upfront:\nHow to return on success, how to return on failure, and where to intercept invalid input.\n📌 Technical Profile Unified Exception and API Encapsulation Architecture\nThrough a unified response model, unified exception hierarchy, and unified input validation mechanism, all API endpoints in the system share a consistent approach to input, output, and error handling.\nHere are three common core concepts:\nAPI Response Envelope: The API response wrapper, i.e., defining the structure all endpoints return uniformly; Global Exception Handling: Centralized exception handling, rather than each endpoint writing its own; Validator: A validator used to check format, range, and validity before data enters business logic. 💡 One-Sentence Understanding Think of an API as airport security.\nAfter a passenger enters the airport:\nCheck ID ↓ Check luggage ↓ Passes all rules ↓ Enter boarding area If there\u0026rsquo;s a problem:\nID error Luggage violation Identity issue Each gate doesn\u0026rsquo;t decide on its own:\n\u0026ldquo;What should we do with this person?\u0026rdquo;\nInstead, a unified security process handles it.\nSoftware systems are the same:\nUser request ↓ Parameter validation ↓ Business processing ↓ Unified response When an exception occurs:\nBusiness exception System exception ↓ Global exception handler ↓ Standard error response This is especially important for AI. Because without rules, AI easily:\nReinvents a new response format and error handling approach for every endpoint it writes.\nI. Negative Example: AI\u0026rsquo;s \u0026ldquo;Free-Style\u0026rdquo; Suppose we ask AI to implement a create user endpoint:\nUsername must be at least 3 characters, password must meet security rules. If the username already exists, provide an error message.\nWithout architectural constraints, AI might write:\n@router.post(\u0026#34;/users\u0026#34;) async def create_user(data: dict): if len(data[\u0026#34;username\u0026#34;]) \u0026lt; 3: return { \u0026#34;success\u0026#34;: False, \u0026#34;message\u0026#34;: \u0026#34;username too short\u0026#34; } if len(data[\u0026#34;password\u0026#34;]) \u0026lt; 8: return { \u0026#34;code\u0026#34;: 400, \u0026#34;error\u0026#34;: \u0026#34;invalid password\u0026#34; } user = await db.get_user(data[\u0026#34;username\u0026#34;]) if user: raise HTTPException( status_code=400, detail=\u0026#34;user already exists\u0026#34; ) try: new_user = await db.create_user(data) return { \u0026#34;result\u0026#34;: new_user, \u0026#34;status\u0026#34;: \u0026#34;ok\u0026#34; } except Exception as e: return { \u0026#34;error\u0026#34;: str(e) } The functionality seems complete.\nBut many problems have already emerged inside.\nProblem 1: Inconsistent Response Formats Within the same endpoint, there\u0026rsquo;s even:\n{ \u0026#34;success\u0026#34;: false } And:\n{ \u0026#34;code\u0026#34;: 400 } As well as:\n{ \u0026#34;status\u0026#34;: \u0026#34;ok\u0026#34; } Every time the frontend calls an endpoint, it has to guess again:\nShould I check success, code, or status this time?\nProblem 2: Parameter Validation Scattered in Business Code if len(data[\u0026#34;username\u0026#34;]) \u0026lt; 3: if len(data[\u0026#34;password\u0026#34;]) \u0026lt; 8: These essentially belong to:\nIs the input valid?\nYet they\u0026rsquo;re mixed into business logic.\nThe next AI writing a \u0026ldquo;modify user\u0026rdquo; endpoint will likely copy the same thing again.\nProblem 3: Inconsistent Exception Handling Username exists:\nraise HTTPException(...) Password error:\nreturn {...} Database exception:\nexcept Exception as e: return {\u0026#34;error\u0026#34;: str(e)} Three types of errors, three different handling approaches.\nProblem 4: Internal System Errors Directly Exposed to Users str(e) Might return:\nDatabase table names SQL statements Server paths Internal configuration Directly to the frontend.\nThis isn\u0026rsquo;t just ugly — it can also introduce security risks.\nProblem 5: Every Endpoint Repeats from Scratch Once the system has 100 endpoints, you might see:\n100 sets of parameter checks 100 sets of try / except 20 response structures 10 error formats The real problem isn\u0026rsquo;t that AI can\u0026rsquo;t write code.\nIt\u0026rsquo;s that:\nWithout unified standards, AI will very efficiently produce inconsistency.\nII. Architecture Rules: Humans Draw the \u0026ldquo;Blueprint\u0026rdquo; First To solve this problem, the entire request flow can be standardized:\nClient request ↓ Schema / Validator Input validation ↓ API ↓ Service ↓ Business exception ↓ Global Exception Handler Global exception handling ↓ ApiResponse Unified response Then give AI a few clear rules.\nRule 1: All Standard APIs Use a Unified Response Structure For example, unify on:\n{ \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: {} } The three fields each have their own role:\ncode Business / status code message Human-readable status message data The actual returned data On success:\n{ \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: { \u0026#34;id\u0026#34;: 12, \u0026#34;username\u0026#34;: \u0026#34;tom\u0026#34; } } On failure:\n{ \u0026#34;code\u0026#34;: 404, \u0026#34;message\u0026#34;: \u0026#34;User \u0026#39;12\u0026#39; not found\u0026#34; } The frontend doesn\u0026rsquo;t have to guess anymore.\nRule 2: APIs Should Not Build JSON Ad Hoc Everywhere Don\u0026rsquo;t:\nreturn { \u0026#34;success\u0026#34;: True, \u0026#34;result\u0026#34;: data } Don\u0026rsquo;t either:\nreturn { \u0026#34;status\u0026#34;: \u0026#34;ok\u0026#34;, \u0026#34;payload\u0026#34;: data } Use uniformly:\nreturn ApiResponse(data=data) This way the response format has only one definition.\nRule 3: Input Format Is Handled by Schema and Validator For example:\nclass UserCreate(BaseModel): username: str = Field( ..., min_length=3, max_length=100 ) Here, Field can be understood as:\nField rules.\nIt directly specifies:\nusername Minimum 3 characters Maximum 100 characters AI doesn\u0026rsquo;t need to rewrite in every API:\nif len(username) \u0026lt; 3: Rule 4: Complex Fields Use Validators Some validations can\u0026rsquo;t be done with simple length checks.\nFor example, a password might require:\nMinimum number of characters Contains uppercase and lowercase letters Contains digits Contains special characters This is when it\u0026rsquo;s appropriate to use:\nValidator\nConcentrating the password rules in one place.\nLater, for:\nCreate user Reset password Change password All reuse the same rules.\nRule 5: Business Errors Use Unified Exception Types For example:\nUser not found Agent not found Knowledge base not found They all belong to:\nNot Found (resource doesn\u0026rsquo;t exist)\nCan uniformly use:\nNotFoundError And:\nUsername already exists Agent name is duplicated Can uniformly be classified as:\nAlreadyExistsError This way the Service only needs to express:\n\u0026ldquo;What business error occurred.\u0026rdquo;\nWithout worrying about whether HTTP should return 404, 409, or another status code.\nRule 6: Exceptions Are Handled Centrally Globally Business code can:\nraise NotFoundError(...) The global exception handler is responsible for:\nNotFoundError ↓ HTTP 404 ↓ ApiResponse Instead of every endpoint:\ntry: ... except: ... Repeated dozens of times.\nIII. What Are the Benefits of Doing This? For traditional development, this is called engineering standards. For AI programming, it has even more direct value.\n1. AI No Longer Arbitrarily Creates Response Structures The project only has:\nApiResponse This one set of rules.\nAfter AI sees existing code, it\u0026rsquo;s more likely to continue writing:\nreturn ApiResponse(data=result) 2. Frontend Calls Become Very Simple The frontend can uniformly assume:\ncode message data Always exist.\nSo the unified HTTP Client can handle:\nSuccess Error Token expired Notification messages Without each page needing to adapt individually.\n3. Validation Rules Don\u0026rsquo;t Scatter For example, if the password rule changes:\nMinimum 8 characters becomes minimum 12.\nIf the rule is concentrated in a Validator, change it in one place.\nIf it\u0026rsquo;s scattered across:\nRegistration Create user Change password Reset password Admin backend Five endpoints, it\u0026rsquo;s easy to miss one.\n4. Service Becomes Cleaner Business code doesn\u0026rsquo;t need to repeatedly:\ntry: ... except HTTPException: ... It only handles business:\nUser not found ↓ Raise NotFoundError User already exists ↓ Raise AlreadyExistsError How errors convert to HTTP is handled uniformly by the outer layer.\n5. AI Can More Easily Understand \u0026ldquo;Which Error Belongs Where\u0026rdquo; Very clear boundaries can be established:\nInput format error → Validator Business rule error → Domain Error Unknown system error → Global Exception Handler Response format → ApiResponse This is essentially layering error handling as well.\nIV. Prompt Implementation: Teach the Rules to AI Just telling AI:\n\u0026ldquo;Pay attention to exception handling.\u0026rdquo;\nHas almost no practical effect.\nWhat\u0026rsquo;s more effective is to write it as concrete engineering rules.\nFor example, add to project-level rules:\n## API Response and Validation Rules All standard JSON APIs must use the project\u0026#39;s unified ApiResponse response envelope. Standard response fields: - code - message - data Rules: - Do not create alternative response structures such as success/result/payload/status unless explicitly required by an existing protocol. - API routes should return ApiResponse, not manually construct response dictionaries. - Request payloads must use existing Pydantic schemas. - Simple input constraints (e.g., length, range, and required fields) should be declared in Pydantic field definitions. - Reusable or complex validation rules should use the project\u0026#39;s existing validators. - Do not duplicate validation logic in API routes. - Business errors must use the project\u0026#39;s domain exception types, e.g., NotFoundError and AlreadyExistsError. - Do not convert business errors to HTTPException in services. - Do not add repetitive try/except blocks to every API route. - Let the global exception handling mechanism convert known exceptions into standardized API responses. - Unexpected internal exceptions must not expose sensitive implementation details in production. Later, when asking AI to develop an endpoint, you can further prompt:\nImplement the \u0026#34;create user\u0026#34; endpoint. Please strictly follow the project\u0026#39;s existing unified API standards: 1. Request parameters use existing Pydantic Schema; 2. Field length, range, and similar rules go in Schema / Validator; 3. API responses uniformly use ApiResponse; 4. Business errors like \u0026#34;user already exists\u0026#34; use existing Domain Errors; 5. Do not repeat try/except in the Router; 6. Do not create new error response formats on your own; 7. Prioritize reusing the project\u0026#39;s existing Validators and exception types. Before implementing, first check: app/schemas/common.py Related business Schemas Existing Services Global exception handling code. Now AI is no longer \u0026ldquo;freely designing an endpoint,\u0026rdquo; but rather:\nAdding an endpoint under the existing exception and response protocol.\nV. Positive Output: How Does miniagent Do It? miniagent has already combined:\nUnified response Business exceptions Global exception handling Pydantic parameter validation into one cohesive system, as shown in the diagram below:\nflowchart TD EX[\"Python ExceptionPython Exception Base Class\"] BASE[\"BaseDomainErrorBusiness Exception Base Class\"] NOTFOUND[\"NotFoundErrorResource Not Found\"] EXISTS[\"AlreadyExistsErrorResource Already Exists\"] EMPTY[\"EmptyDataErrorData Is Empty\"] BAD[\"BadRequestErrorBad Request\"] READONLY[\"ReadOnlyErrorResource Is Read-Only\"] INVALID[\"InvalidValueErrorInvalid Value\"] HANDLER[\"Global Exception HandlerGlobal Exception Handling\"] RESPONSE[\"ApiResponseUnified Response Formatcode · message · data\"] EX --\u003e BASE BASE --\u003e NOTFOUND BASE --\u003e EXISTS BASE --\u003e EMPTY BASE --\u003e BAD BASE --\u003e READONLY BASE --\u003e INVALID NOTFOUND --\u003e HANDLER EXISTS --\u003e HANDLER EMPTY --\u003e HANDLER BAD --\u003e HANDLER READONLY --\u003e HANDLER INVALID --\u003e HANDLER HANDLER --\u003e RESPONSE 1. Unified Response Value: ApiResponse miniagent defines the unified top-level response model in backend/app/schemas/common.py:\nclass ApiResponse(BaseModel, Generic[T]): \u0026#34;\u0026#34;\u0026#34; Generic top-level API response envelope. \u0026#34;\u0026#34;\u0026#34; code: int = Field( 200, description=\u0026#34;Business status code, 200 = success\u0026#34; ) message: str = Field( \u0026#34;success\u0026#34;, description=\u0026#34;Human-readable status message\u0026#34; ) data: Optional[T] = Field( None, description=\u0026#34;Response payload\u0026#34; ) That is, standard endpoints uniformly revolve around:\n{ \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: {} } It also uses:\nGeneric[T] Generic means generics.\nIt can be simply understood as:\ndata can hold different types of data, but the outer code / message / data shell remains unchanged.\nFor example:\nApiResponse[UserOut] ApiResponse[AgentOut] ApiResponse[PageResult] Different inner data, unified outer protocol.\n2. Paginated Results Are Also Uniformly Encapsulated The same file also defines:\nclass PageResult(BaseModel, Generic[T]): total: int page: int page_size: int data: List[T] So paginated endpoints don\u0026rsquo;t return:\nrows count current today and then:\nitems total pageNum tomorrow.\nInstead, they uniformly use:\n{ \u0026#34;total\u0026#34;: 100, \u0026#34;page\u0026#34;: 1, \u0026#34;page_size\u0026#34;: 20, \u0026#34;data\u0026#34;: [] } This is especially important for AI, because when adding a paginated endpoint, it can directly reuse the existing structure.\n3. Business Exceptions Also Have a Unified Base Class miniagent defines:\nclass BaseDomainError(Exception): \u0026#34;\u0026#34;\u0026#34; Business Logic Exception Base Class \u0026#34;\u0026#34;\u0026#34; That is:\nThe base class for domain business exceptions.\nThen continues to derive:\nclass NotFoundError(BaseDomainError): ... class AlreadyExistsError(BaseDomainError): ... class EmptyDataError(BaseDomainError): ... class ReadOnlyError(BaseDomainError): ... class InvalidValueError(BaseDomainError): ... This way, when business code encounters errors, it doesn\u0026rsquo;t need to create:\nUserNotExistException MissingAgentException KBNotFoundException NoDocumentException Various entirely different error hierarchies. Instead, it tries to fit into existing semantics.\n4. Error Messages Are Also Unified with I18n Here, I18n means:\nInternationalization.\nBaseDomainError can use:\ndef to_detail(self) -\u0026gt; str: return _translate(...) To convert exceptions into messages in the corresponding language.\nThis means:\nBusiness exception ↓ Unified error type ↓ Unified internationalized message Instead of each AI hardcoding a new message when writing an endpoint:\n\u0026#34;User not found\u0026#34; VII. Global Exceptions: Only Need to \u0026ldquo;Raise,\u0026rdquo; Not \u0026ldquo;Catch\u0026rdquo; Everywhere miniagent provides in the application entry point:\ndef handle_exception(exc: Exception) -\u0026gt; JSONResponse: Which uniformly handles different exception types, for example:\nService ↓ NotFoundError ↓ Global Exception Handler ↓ HTTP 404 ↓ ApiResponse For:\nAlreadyExistsError It uniformly converts to:\nHTTP 409 Conflict Where Conflict means:\nResource state conflict.\nFor example, creating a username that already exists fits this semantics well.\nUnknown Exceptions Also Have a Unified Fallback If it\u0026rsquo;s not a known business exception:\nerror_data = { \u0026#34;error\u0026#34;: str(exc) if settings.debug else t(\u0026#34;common.error_500\u0026#34;) } This reflects a very important production environment principle:\nDevelopment environment:\nCan see detailed errors Convenient for debugging Production environment:\nHide internal implementation Return unified error message Avoiding directly exposing internal exceptions to regular users.\nVI. Validator: Invalid Input Should Not Enter the Business Layer Let\u0026rsquo;s look at miniagent\u0026rsquo;s user Schema.\nIt doesn\u0026rsquo;t write inside the create user API:\nif len(username) \u0026lt; 3: Instead, it directly defines:\nclass UserCreate(BaseModel): username: str = Field( ..., min_length=3, max_length=100 ) nickname: Optional[str] = Field( None, max_length=100 ) avatar: Optional[str] = Field( None, max_length=500 ) This means:\nWhen the username length is invalid, the request is intercepted by the data model before it even enters the core business logic.\nPassword Validation Further Reuses the Validator miniagent currently defines:\nPasswordValue = Annotated[ str, Field(max_length=128), AfterValidator(validate_password) ] Two terms appear here.\nAnnotated Annotated can be understood as:\nAttaching additional rules to a data type.\nHere the base type is still:\nstr But with the addition of:\nMaximum length 128 + Password Validator AfterValidator AfterValidator can be understood as:\nAfter base type validation completes, execute a custom validation function.\nHere it calls:\nvalidate_password So creating a user:\nclass UserCreate(BaseModel): password: PasswordValue Resetting a password:\nclass UserPasswordReset(BaseModel): password: PasswordValue Both reuse the same password rules.\nThis is a textbook example of:\nDefine once, reuse everywhere.\nVII. Ultimately Forming a Very Clear Request Chain Combining miniagent\u0026rsquo;s actual implementations, we get:\nNow different responsibilities become very clear:\nSchema / Validator Responsible for \u0026#34;is the input valid\u0026#34; Service Responsible for \u0026#34;can the business do this\u0026#34; Domain Error Responsible for \u0026#34;what business problem occurred\u0026#34; Global Exception Handler Responsible for \u0026#34;how errors convert to HTTP responses\u0026#34; ApiResponse Responsible for \u0026#34;what the final return looks like\u0026#34; This is the true value of a unified exception architecture.\nVIII. How Will AI Write After Architecture Empowerment? Suppose we now tell AI:\n\u0026ldquo;Add a feature to modify the username.\u0026rdquo;\nWithout architecture, AI might design from scratch:\nParameter checks Return JSON try / except Error messages With rules like miniagent\u0026rsquo;s, it should first think:\n1. Does UserUpdate already have a username field? ↓ 2. Does Field already specify length? ↓ 3. Add business operation in UserService ↓ 4. Raise AlreadyExistsError on duplicate name ↓ 5. Router calls Service ↓ 6. Return ApiResponse Rather than reinventing a set of rules.\nThis is what we call:\nAI output after architecture empowerment.\nIt\u0026rsquo;s not that AI suddenly got \u0026ldquo;smarter,\u0026rdquo; but rather:\nWe reduced the things it can freely decide.\nConclusion Unified exception and API encapsulation may look like just a few unassuming base classes:\nApiResponse PageResult BaseDomainError Validator But for AI programming, they actually establish a very important set of \u0026ldquo;traffic rules.\u0026rdquo;\nIt tells AI:\nHow to validate input How to return on success How to express business errors Where system errors are handled What the frontend ultimately sees Without these rules, AI will freely improvise in every endpoint. And the better AI gets at writing code, the faster this \u0026ldquo;inconsistency\u0026rdquo; accumulates.\nTherefore:\nGood exception architecture isn\u0026rsquo;t about adding a few base classes to the code. It\u0026rsquo;s about ensuring the entire system has only one error language and one API language.\nIn the era of AI programming, these standards should be固化 into Project Rules, System Prompts, or project context upfront.\nUltimately forming:\nInvalid input → Intercepted by Validator Business disallowed → Expressed as Domain Error System exception → Caught by Global Handler fallback Whether success or failure → Returned via unified protocol This way, every new endpoint AI adds is actually reusing the same engineering rules.\nDon\u0026rsquo;t let AI reinvent \u0026ldquo;what success means and what failure means\u0026rdquo; for every endpoint it writes.\nFirst unify the rules, then let AI write business. This is the true value of unified exception and encapsulation architecture in AI programming.\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0106/","summary":"\u003cp\u003eAI is very good at quickly writing API endpoints. For example, if you tell it:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Add an endpoint to create a user.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eIt can probably produce working code in a few minutes. But without unified standards in the project, different endpoints will quickly end up like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;success\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAnother endpoint returns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;code\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;msg\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;ok\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;result\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eWhen yet another endpoint encounters an error, it directly returns:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"nt\"\u003e\u0026#34;error\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;user not found\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSome places even throw raw database exceptions back to the frontend.\u003c/p\u003e\n\u003cp\u003eEvery single endpoint \u0026ldquo;works,\u0026rdquo; but the entire system becomes increasingly hard to maintain.\u003c/p\u003e\n\u003cp\u003eSo, there\u0026rsquo;s a very important but often overlooked category of foundational architecture in AI programming:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eUnified response values + global exception handling + unified parameter validation.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eTheir purpose is to define upfront:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eHow to return on success, how to return on failure, and where to intercept invalid input.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e","title":"Exception and Unified Encapsulation Architecture: Standardizing Unified Return Values, Global Exception Handling, and Parameter Validation"},{"content":"AI writing code has one very common problem:\nIt knows how a feature should be implemented, but doesn\u0026rsquo;t necessarily know where the code should go.\nFor example, if you tell AI:\n\u0026ldquo;Add a delete Agent feature.\u0026rdquo;\nWithout a clear project structure, it might directly, inside the API endpoint: query the database, evaluate business rules, clear the cache\u0026hellip;\nThe feature might work quickly. But as AI writes code this way again and again, the project will eventually become:\nBusiness logic inside the API Database operations inside Services Business judgments inside Repositories Utility classes referenced everywhere At this point, we need another very fundamental yet extremely important architectural concept:\nLayering.\n📌 Technical Profile Layered Architecture\nDivide the system into several layers based on different responsibilities — such as the interface layer, business layer, data access layer, and infrastructure layer — and define what each layer is responsible for and which layers it can call.\nWhile MVC and Clean Architecture differ in specific form, they all share a common idea:\nDon\u0026rsquo;t mix different types of code together.\n💡 One-Sentence Understanding Think of a software system as a restaurant.\nCustomers don\u0026rsquo;t run directly into the storeroom to grab ingredients, and waiters don\u0026rsquo;t run into the kitchen to cook.\nIt usually goes:\nCustomer ↓ Waiter ↓ Chef ↓ Storeroom Each layer has its own job.\nSoftware is the same:\nUser request ↓ API / Controller ↓ Service ↓ Repository ↓ Database The most important thing isn\u0026rsquo;t \u0026ldquo;creating a few more folders.\u0026rdquo;\nIt\u0026rsquo;s:\nWho handles requests, who handles business, who handles the database — these must be clearly defined upfront.\nFor AI, this is like drawing clear floors in a building:\nYou can work on your own floor, but don\u0026rsquo;t casually walk through walls.\nI. Why Does AI Especially Need \u0026ldquo;Layering\u0026rdquo;? When a human developer sees a piece of code, they often judge from experience:\n\u0026ldquo;This SQL shouldn\u0026rsquo;t be in the Controller.\u0026rdquo;\nAI, without explicit rules, might think:\n\u0026ldquo;Writing it here is fastest, and it gets the task done.\u0026rdquo;\nIt\u0026rsquo;s more concerned with whether the current problem is solved than whether the entire project will become messy six months later.\nSo if a project has no clear layering, AI easily gradually writes:\nAPI ├── Parameter validation ├── Permissions ├── Business logic ├── SQL ├── Caching └── Third-party APIs Eventually a single endpoint file is hundreds or even thousands of lines long. What layered architecture truly solves is:\nFirst define where code should live.\nII. Architecture Standards: Give Each Layer Clear Responsibilities An easy-to-understand web backend can be simplified into these layers:\nAPI / Controller ↓ Service ↓ Repository ↓ Database More complex systems can also add:\nSchema Runtime Infrastructure But the principle remains unchanged.\n1. API Layer: Responsible for \u0026ldquo;Reception\u0026rdquo; The API layer is mainly responsible for:\nReceiving requests Parameter transformation Identity / permission entry point Calling Service Returning results It should not be responsible for core business logic.\nFor example:\n@router.delete(\u0026#34;/{agent_id}\u0026#34;) async def delete_agent( agent_id: int, svc: AgentService = Depends(get_service), ): await svc.delete_agent(agent_id) return ApiResponse() From a readability perspective, this is very simple:\nReceive request ↓ Call AgentService ↓ Return result This is a healthy API layer.\n2. Service Layer: Responsible for \u0026ldquo;Business\u0026rdquo; The Service answers the question:\nHow should this be done?\nFor example, deleting an Agent might be more than just:\nDELETE FROM agents It might also include:\nCheck if the Agent exists Delete data Clean up relationships Invalidate cache Record business result These all belong to the business process.\nTherefore, they should be concentrated in the Service, not scattered across the API.\n3. Repository / Data Access Layer: Responsible for \u0026ldquo;How to Get Data\u0026rdquo; The Repository is responsible for:\nQuerying Inserting Updating Deleting Transactions Database access It\u0026rsquo;s more concerned with:\nHow data is stored and queried.\nThan with:\nWhy this business operation should be done this way.\nFor example:\nService: After deleting Agent, clear the cache Repository: Execute the Agent deletion operation The two have different responsibilities.\n4. Schema / DTO: Responsible for What Data Looks Like For example:\nAgentCreate AgentUpdate AgentOut They are responsible for describing:\nInput parameters Output data Field types Validation structures This prevents various dict objects from freely roaming through the project.\n5. Runtime: Responsible for Complex Execution Capabilities For ordinary CRUD management features, this layer may not need to exist independently. But systems like AI Agent platforms, RAG, and workflow engines typically have:\nAgentRunner Retrieval Pipeline LLM Runtime Conversation Runtime Tool Execution These are neither ordinary database CRUD operations nor simple APIs, so they can independently form a Runtime layer.\n6. Infrastructure: Responsible for Technical Infrastructure Infrastructure typically includes:\nDatabase connections Caching Logging Configuration Event bus External service connections File storage These are technical capabilities the system needs to run. Business code should use them.\nIII. What Truly Matters: Calls Must Have Direction Simply creating:\napi/ services/ repositories/ A few directories isn\u0026rsquo;t enough. More importantly, you must define:\nWhich layers can call which layers.\nThe easiest rule to understand is:\nAPI ↓ Service ↓ Repository ↓ Database And not become:\nAPI ───────→ Database ↑ ↓ Repository ← Service Otherwise, even though the directories are layered, the code is still a mess.\nSo we can give AI several very direct rules.\nRule 1: API Must Not Directly Access the Database Wrong:\n@router.delete(\u0026#34;/{id}\u0026#34;) async def delete(id: int): await db.execute(...) Recommended:\nawait service.delete(id) Rule 2: API Must Not Carry Core Business Logic Don\u0026rsquo;t write:\nif agent.is_active: ... if has_tools: ... if user.role: ... await db... cache.clear() These should go into the Service.\nRule 3: Repository Must Not Make Business Decisions The Repository can:\nQuery Agent Delete Agent Update Agent But try not to decide inside it:\n\u0026ldquo;Admins can\u0026rsquo;t delete the default Agent.\u0026rdquo;\nThis is a business rule and belongs better in the Service.\nRule 4: Upper Layers Call Lower Layers Through Stable Interfaces For example:\nAPI ↓ AgentService.delete_agent() The API doesn\u0026rsquo;t need to know what exactly goes on inside the Service:\nHow many Repositories are called Whether cache is cleared Whether events are published This way, when the Service\u0026rsquo;s internals change, the API can remain stable.\nRule 5: Cross-Layer Calls for Convenience Are Prohibited This is an especially important rule for AI.\nFor example, if AI discovers in the API:\ncontainer.agent_db It might call it directly inside the Router.\nJust because it\u0026rsquo;s \u0026ldquo;accessible\u0026rdquo; doesn\u0026rsquo;t mean it \u0026ldquo;should be used.\u0026rdquo;\nArchitecture rules should be explicit:\nAccessible does not equal allowed.\nIV. What Are the Benefits of Doing This? The benefits of layering aren\u0026rsquo;t just that the code looks prettier. For AI programming, it directly affects long-term code quality.\n1. AI Can More Easily Determine Where Files Should Go When AI needs to add:\nAn Agent query feature\nIt can determine:\nHTTP interface → api Business rules → services Database queries → repositories Input/output models → schemas No need to redesign the project structure every time.\n2. Smaller Modification Scope If only modifying:\nAgent business rules\nUsually focus on checking:\nAgentService Without having to overturn the API, database, and frontend entirely.\nThis makes it easier for AI to execute \u0026ldquo;small-scope modifications.\u0026rdquo;\n3. Easier to Test Once the Service doesn\u0026rsquo;t depend on HTTP, it can be tested independently. The Repository can be tested independently against the database.\nThe API can test:\nAre routes correct? Are permissions correct? Are inputs/outputs correct? The testing targets become very clear.\n4. Smaller Impact When Swapping Technology Implementations For example, later:\nSQLite ↓ PostgreSQL Ideally, the main modification is in:\nRepository / Infrastructure Rather than rewriting the business code alongside it.\nSimilarly, if:\nFastAPI is replaced with a different web framework in the future, the core business layer shouldn\u0026rsquo;t need to be entirely rewritten either.\n5. AI Can More Easily Understand Existing Projects For AI, the directory itself is a form of information.\nSeeing:\napi/ services/ repositories/ schemas/ runtime/ infra/ It can immediately infer:\nThis is a system with clear responsibility layering.\nMuch easier to understand than all Python files piled into:\napp/ 6. Multiple AIs Can More Easily Stay Consistent Use one model today, switch to another tomorrow, and perhaps use a Coding Agent to auto-modify code the day after.\nAs long as architecture rules are stable:\nAPI is API Service is Service Repository is Repository Different AIs\u0026rsquo; coding styles may differ, but the overall project structure won\u0026rsquo;t easily drift.\nV. Prompt Implementation: Write Layering Rules for AI Just telling AI:\n\u0026ldquo;Use Clean Architecture.\u0026rdquo;\nIs still too abstract.\nWhat\u0026rsquo;s truly useful is clear responsibilities and call direction. For example, you can place the following rules in your project rules:\n## Backend Layering Rules The backend follows a strict layered architecture. Layers: - app/api: HTTP routing, request parsing, authorization entry point, dependency injection, response transformation. - app/services: Business logic and use case orchestration. - app/repositories: Database access and persistence operations. - app/schemas: Request/response DTOs and validation models. - app/runtime: Agent, conversation, LLM, retrieval, and other long-running runtime capabilities. - app/infra: Database, cache, logging, configuration, storage, and infrastructure integration. Rules: - API routes must not directly access the database. - API routes must delegate business operations to services. - Services must not depend on FastAPI requests or HTTP details. - Repository code should focus on persistence, not business rules. - Do not bypass services just because repository or database objects are directly accessible. - Prefer reusing existing services and repositories before creating new ones. - Keep dependencies flowing within the established architecture. - Before writing code, identify the correct layer for each responsibility. Then the task prompt can be written like this:\nAdd a delete feature for Agent. Strictly follow the project\u0026#39;s existing layered architecture: API only handles routing, permissions, and responses; Business logic goes in AgentService; Database operations reuse the existing data access layer; Do not directly access the database or handle caching in the Router. Before implementing, first check the existing Agent API, Service, Schema, and data access code. Now AI gets a very clear construction route:\nFirst find the API ↓ Then find the Service ↓ When data is needed, find the Repository ↓ When infrastructure is needed, go through existing capabilities Rather than:\n\u0026ldquo;Whatever object I can reach, I\u0026rsquo;ll call.\u0026rdquo;\nVI. Positive Output: What Does miniagent\u0026rsquo;s Actual Layering Look Like? Take the actual project miniagent as an example.\nminiagent\u0026rsquo;s current backend isn\u0026rsquo;t a traditional three-tier MVC (Model, View, Controller) architecture, but rather uses the following layered architecture to address the complexity of an Agent system:\nflowchart TD UI[\"Frontend ApplicationsManagement / Workplace\"] API[\"Interface Layer — API Layerapp/apiRouting · Parameter Parsing · Permission Entry · Response Transformation\"] SERVICE[\"Business Service Layerapp/servicesBusiness Logic · Use Case Orchestration · Cross-Module Coordination\"] RUNTIME[\"Runtime Layerapp/runtimeAgentRunner · Conversation · LLM · Retrieval · Tool\"] REPO[\"Data Access Layer — Repository Layerapp/repositoriesQuery · Insert · Update · Delete · Persistence\"] SCHEMA[\"Data Models — Schema / DTOapp/schemasRequest Models · Response Models · Data Validation\"] CORE[\"Core Capabilitiesapp/coreConfiguration · Security · DI · i18n · Logging\"] INFRA[\"Infrastructure Layerapp/infraORM · Database Initialization · Cache · Storage\"] DATA[\"Data \u0026 External ResourcesSQLite · DuckDB · ChromaDB · BM25 · Files · LLM APIs\"] UI --\u003e|\"REST / SSE\"| API API --\u003e SERVICE API -.-\u003e SCHEMA API -.-\u003e CORE SERVICE --\u003e RUNTIME SERVICE --\u003e REPO SERVICE -.-\u003e SCHEMA SERVICE -.-\u003e CORE RUNTIME --\u003e REPO RUNTIME -.-\u003e CORE RUNTIME --\u003e INFRA REPO --\u003e INFRA INFRA --\u003e DATA CORE -.-\u003e INFRA It can be simplified to:\nflowchart TD API[\"Interface Layerapp/apiHTTP Routing · Request / Response Handling\"] SERVICE[\"Business Service Layerapp/servicesBusiness Logic · Use Case Orchestration\"] RUNTIME[\"Runtime / Data Access Layerapp/runtime · app/repositoriesAgent Runtime · Data Access\"] INFRA[\"Infrastructure / Data Layerapp/infraSQLite · DuckDB · ChromaDB · File Storage\"] API --\u003e SERVICE SERVICE --\u003e RUNTIME RUNTIME --\u003e INFRA This isn\u0026rsquo;t about making directories look pretty — it\u0026rsquo;s about telling developers and AI:\nWhich layer different code should work in.\n1. The API Layer Explicitly Declares: Only HTTP Is Handled Here miniagent\u0026rsquo;s current Agent API file:\nbackend/app/api/admin/agent.py The file begins with:\n# Agent API Router – HTTP layer only, # all logic lives in AgentService This sentence itself is actually excellent AI architecture prompting:\nOnly HTTP is handled here. All business logic goes into AgentService.\nFor example, deleting an Agent:\n@router.delete( \u0026#34;/{agent_id}\u0026#34;, response_model=ApiResponse, summary=\u0026#34;Delete agent [agent:delete]\u0026#34; ) async def delete_agent( agent_id: int, svc: AgentService = Depends(get_service), caller_id: int = Depends(_delete), ): await svc.delete_agent(agent_id) return ApiResponse() You can see the Router does very little:\nReceive agent_id ↓ Permission check ↓ Obtain AgentService ↓ Call delete_agent() ↓ Return ApiResponse It doesn\u0026rsquo;t do the following on its own:\nManipulate Agent database Clean up cache Implement Agent business rules This is a textbook example of \u0026ldquo;keeping the API layer in check.\u0026rdquo;\n2. Business Logic Goes into AgentService Correspondingly:\nbackend/app/services/admin/agent.py The file also explicitly states at the beginning:\n# Agent Service – business logic layer # (no HTTP / FastAPI imports) And AgentService\u0026rsquo;s description:\nclass AgentService: \u0026#34;\u0026#34;\u0026#34; Encapsulates all business logic for the Agent resource. \u0026#34;\u0026#34;\u0026#34; That is:\nThe Service layer explicitly does not depend on HTTP / FastAPI, and is responsible for Agent business logic.\nThis is a very important boundary. If AgentService is later used in:\nHTTP API Background tasks Scripts Tests Other internal services It doesn\u0026rsquo;t need to know:\nWhether the current request is coming from FastAPI.\n3. The Business Action of Deleting an Agent Also Happens in the Service For example:\nasync def delete_agent( self, agent_id: int ) -\u0026gt; None: await self._agent_db.delete_agent(agent_id) self._cache.on_agent_changed(agent_id) Here you can see the distinction in responsibilities.\nThe API layer only knows:\nI want to delete an Agent The Service knows:\nDelete the Agent + Invalidate relevant caches after the Agent changes If later we add:\nRecord audit log Publish event Clean up associated resources These business actions can still be orchestrated by the Service, without the API becoming increasingly bloated.\n4. The Service Then Calls Data Access Capabilities When AgentService initializes, it obtains:\nself._agent_db = container.agent_db self._user_agent_relation_db = container.user_agent_relation_db self._agent_tool_relation_db = container.agent_tool_relation_db self._tool_db = container.tool_db self._cache = container.object_cache_invalidator Thus forming a very clear call chain:\nAgent API ↓ AgentService ↓ Agent DB / Relation DB ↓ Database Meanwhile, caching is also handled through existing infrastructure capabilities:\nAgentService ↓ Object Cache Invalidator Rather than the API doing:\nredis.delete(...) on its own.\n5. Complex Capabilities Enter Runtime Separately miniagent differs from ordinary management systems in another way:\nIt genuinely needs to run:\nAgent LLM RAG Retrieval Conversation Tool SQL Agent Therefore, the project places these long-running or execution-oriented capabilities separately in:\napp/runtime/ Runtime contains Agent, Session, LLM, Retrieval, and other runtime components.\nThis way, things like:\nAgentRunner RetrievalPipeline LLM Client aren\u0026rsquo;t forcibly crammed into ordinary Services.\nThis is also an important layering concept:\nArchitecture should serve business complexity, not mechanically follow templates.\nVII. What\u0026rsquo;s the Difference Between Layering and the Previous Article on DDD? These two concepts are very easy to mix up. They can be distinguished by two questions. DDD mainly answers:\nWhose business is this?\nFor example:\nAgent Knowledge Base Tool Conversation User These are business boundaries.\nLayered architecture mainly answers:\nWhich layer does this code belong to?\nFor example:\nAPI Service Repository Runtime Infrastructure These are technical responsibility boundaries.\nA simple way to understand it:\nDDD Solves horizontal boundaries Layered Architecture Solves vertical boundaries When the two are combined, AI gets an even clearer map:\nFirst determine: Which domain is this? Then determine: Which layer of code is this? Conclusion Layered architecture may look like just a few directories:\napi/ services/ repositories/ But its true meaning goes far beyond that. It\u0026rsquo;s constantly telling AI:\nDon\u0026rsquo;t write business logic where requests are received;\nDon\u0026rsquo;t care about HTTP where business logic is written;\nGo through the data access layer when you need data;\nReuse unified capabilities when you need infrastructure.\nThe stronger AI\u0026rsquo;s coding ability and the more files it can modify at once, the more important these boundaries become. Otherwise, one wrong \u0026ldquo;convenient call\u0026rdquo; could quickly be replicated by AI to dozens of places.\nTherefore:\nStandardized directory structure is just the surface. What truly matters is standardized responsibilities and dependency direction.\nFor AI programming, it can be summarized in one very simple sentence:\nDDD tells AI \u0026ldquo;whose responsibility this is.\u0026rdquo; Layered architecture tells AI \u0026ldquo;which layer this should be done in.\u0026rdquo;\nWhen the two are combined, AI truly has an engineering map it can follow for long-term construction.\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0105/","summary":"\u003cp\u003eAI writing code has one very common problem:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eIt knows how a feature should be implemented, but doesn\u0026rsquo;t necessarily know where the code should go.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eFor example, if you tell AI:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Add a delete Agent feature.\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eWithout a clear project structure, it might directly, inside the API endpoint: \u003ccode\u003equery the database\u003c/code\u003e, \u003ccode\u003eevaluate business rules\u003c/code\u003e, \u003ccode\u003eclear the cache\u003c/code\u003e\u0026hellip;\u003c/p\u003e\n\u003cp\u003eThe feature might work quickly. But as AI writes code this way again and again, the project will eventually become:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eBusiness logic inside the API\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eDatabase operations inside Services\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eBusiness judgments inside Repositories\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eUtility classes referenced everywhere\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAt this point, we need another very fundamental yet extremely important architectural concept:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eLayering.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e","title":"Layered Clean Architecture: Standardizing Project Directory Structure to Prevent AI Boundary Violations"},{"content":"Getting AI to write a single feature is usually not difficult. What\u0026rsquo;s truly difficult is:\nAfter AI writes dozens or even hundreds of features in a row, can the code still remain clear?\nMany projects start with a decent structure — a bit of user functionality here, some permission handling there, order features over there, logging features somewhere else.\nAs requirements grow, different modules start calling each other, referencing each other, modifying each other, and may eventually end up like this:\nuser.py ↓ permission.py ↓ agent.py ↓ tool.py ↓ knowledge_base.py ↓ conversation.py ↘ calls user.py again There are plenty of files and lots of code, but who is responsible for what becomes increasingly unclear.\nSoftware architecture has a very vivid name for this:\nBig Ball of Mud\nAnd Domain-Driven Design, or DDD as we often hear, has one very important role: drawing boundaries clearly before the system starts to get messy.\nFor AI programming, this role is especially critical.\n📌 Technical Profile Domain-Driven Design, abbreviated as DDD\nDDD is an approach to organizing software around the business domain.\nIt emphasizes first understanding \u0026ldquo;what different businesses exist in the system,\u0026rdquo; then drawing boundaries for each, so that code from different domains is responsible for its own concerns.\n💡 One-Sentence Understanding Think of a large software system as a large hospital. The hospital has: Registration, Outpatient, Pharmacy\u0026hellip;\nThese departments all belong to the same hospital, but we wouldn\u0026rsquo;t let:\nThe pharmacy directly modify the financial system database,\nThe lab department directly handle doctor scheduling,\nThe billing system directly modify medical records.\nEach department has its own responsibilities, and when collaboration is needed, it happens through clear processes.\nDDD does something very similar:\nFirst identify the \u0026ldquo;business departments\u0026rdquo; in the software, then define what each department is responsible for.\nI. Why Is AI Especially Prone to Writing \u0026ldquo;Big Ball of Mud\u0026rdquo; Code? This has to do with how AI works. AI typically receives isolated, local tasks:\nAdd a user query feature.\nA moment later:\nAdd Tools to the Agent.\nThen later:\nUsers should only see Agents they have permission to use.\nAnd then:\nConversation records need to be linked to Agents.\nEach task looks perfectly reasonable on its own, but AI tends to use whichever approach is most convenient at the moment:\nNeed user data here ↓ Directly call UserRepository Need Agent there ↓ Directly query the Agent table Need permissions here ↓ Add another if-statement Need Tool there ↓ Directly call ToolDatabase Gradually:\nUser Agent Tool Knowledge Base Conversation Permission start interweaving with each other.\nThe problem isn\u0026rsquo;t that AI wrote wrong code, but that:\nAI is very good at solving the current problem, but doesn\u0026rsquo;t necessarily maintain the system\u0026rsquo;s long-term boundaries by nature.\nSo, we need to first tell AI:\nWhose territory this is.\nII. The Most Valuable DDD Concept for AI Programming: Bounded Context DDD has many concepts: Entity, Value Object, Aggregate\u0026hellip;\nFor beginners, there\u0026rsquo;s no need to master everything at once.\nIn AI programming, the most important concept to understand first is:\nBounded Context.\nThe term sounds complex, but it\u0026rsquo;s actually very simple.\nThink of it as:\nThe permitted scope of activity for a certain type of business code.\nFor example, an agent platform naturally contains these business domains:\nUsers \u0026amp; Permissions Agent Management Knowledge Base Tool Conversation LLM System Configuration We can think of them as several rooms. Each room can be complex.\nBut:\nComplexity is fine, chaos is not.\nIII. Architecture Standards: First Tell AI Which Code Belongs to Which Domain Suppose our project contains:\nAgent User Tool Knowledge Base Conversation We can start by establishing some simple rules.\nRule 1: Divide Code by Business Capability, Not by \u0026ldquo;Convenience\u0026rdquo; For example:\nAgent-related business logic Should be concentrated within the Agent domain itself, rather than:\nuser.py contains some Agent logic tool.py contains some Agent logic common.py contains some more Agent logic utils.py contains even more Otherwise, over time:\nYou want to modify Agent, but have no idea how many files you need to change.\nRule 2: One Domain Should Not Arbitrarily Manipulate Another Domain\u0026rsquo;s Data For example, Conversation needs to know:\nWhich Agent the current conversation belongs to.\nIt can store:\nagent_id But this doesn\u0026rsquo;t mean the Conversation module should casually modify the Agent\u0026rsquo;s internal state.\nA better approach is:\nConversation │ │ needs Agent capability ▼ Agent Service / Public Interface Rather than:\nConversation ↓ Directly manipulate Agent\u0026#39;s internal database Rule 3: Cross-Domain Collaboration Must Go Through Explicit Interfaces If Agent needs Tool:\nDon\u0026rsquo;t let Agent directly access Tool data tables everywhere. There should be a clear relationship:\nAgent ↓ Tool Service / Repository ↓ Tool This way AI knows:\n\u0026ldquo;I\u0026rsquo;m currently working on Agent business. If I need Tool, I should access it through existing capabilities, not arbitrarily modify Tool\u0026rsquo;s internal implementation.\u0026rdquo;\nRule 4: Domain Names Must Stay Consistent This is especially important for AI.\nFor example, if the project already uses Agent, don\u0026rsquo;t call it Bot today, have AI create Assistant tomorrow, and then AIWorker the day after\u0026hellip;\nDDD strongly emphasizes one thing:\nUbiquitous Language.\nThat is, the team, code, database, and APIs should all use the same set of business vocabulary.\nFor AI, this is equivalent to reducing ambiguity.\nIV. What Are the Benefits of Doing This? The benefit of DDD for AI programming isn\u0026rsquo;t just \u0026ldquo;the directory looks tidy.\u0026rdquo;\nWhat it truly solves is:\nWhere exactly AI should modify code each time.\n1. AI\u0026rsquo;s Search Scope Becomes Smaller Suppose the user says:\nAdd an enable/disable feature for Agents.\nWithout domain boundaries, AI might search the entire project. With clear boundaries, it should first focus on:\nAgent API Agent Service Agent Repository Agent Schema That is:\nNarrow a global problem into a local one.\n2. AI Is Less Likely to \u0026ldquo;Casually Modify Code\u0026rdquo; Everywhere AI has a very common tendency:\nSince this place can also solve the problem, I\u0026rsquo;ll just modify it casually.\nOnce or twice, you won\u0026rsquo;t notice. After dozens of times, boundaries disappear.\nIf rules are clear:\nConversation logic can only modify Conversation-related code, unless cross-domain collaboration is genuinely needed.\nAI\u0026rsquo;s modification scope becomes more controllable.\n3. Modifying One Domain Won\u0026rsquo;t Easily Damage the Entire System For example, suppose we later refactor the knowledge base.\nIf Knowledge Base has relatively clear boundaries:\nKnowledge Base ├── API ├── Service ├── Retrieval ├── Repository └── Storage Then when refactoring the knowledge base, most of the work can stay within this scope. Rather than one modification causing:\nAgent breaks Conversation breaks User breaks too 4. Newcomers Can Understand the Project More Easily This benefits not only AI but also humans.\nWhen a new developer enters the project, if they see:\nagent knowledge_base tool conversation user They can quickly build an understanding:\nSo the system is primarily composed of these business capabilities.\nRather than having to study hundreds of files first just to understand what the project does.\n5. The Project Becomes More Suitable for Long-Term AI Maintenance The scariest thing in AI programming isn\u0026rsquo;t that the first round of code generation is poor, but that:\nRound 1: AI writes it one way Round 2: AI writes it another way Round 3: A different structure appears Round 4: Everything starts cross-referencing Eventually the entire project has no stable form.\nDomain boundaries act as a constant reminder to AI:\nYou can change things, but don\u0026rsquo;t break boundaries.\nV. Prompt Implementation: Truly Tell AI About Domain Boundaries Therefore, you can\u0026rsquo;t just tell AI:\nThis project uses DDD.\nThis statement has almost no practical binding force. A more effective approach is to write it as executable rules.\nFor example:\n## Domain Boundaries The system is built around business domains. The main domains include: - User \u0026amp; Permission - Agent - Knowledge Base - Tool - Conversation - LLM - System Configuration Rules: - Keep business logic within its owning domain. - Do not place domain logic in utility modules or unrelated modules. - Do not directly modify another domain\u0026#39;s internal data unless the existing architecture explicitly allows it. - Cross-domain operations should go through existing services, repositories, factories, or defined interfaces. - Reuse existing domain terminology. - Do not create differently-named alternative concepts for existing domain objects. - Before implementing a feature, identify which domain it belongs to. - Keep changes within that domain as much as possible. Then when asking AI to develop features going forward, don\u0026rsquo;t just say:\nAdd Tool binding functionality to Agent. Instead:\nAdd Tool binding functionality to Agent. First confirm this feature belongs to the Agent domain. Respect the existing domain boundaries of Agent and Tool, prioritize reusing existing Services, Repositories, and relationship models. Do not put business logic in the API Router. Do not directly manipulate the database in the Router. Now AI gets not just:\nWhat to do.\nBut also:\nIn which context to do it.\nVI. Positive Output: See How miniagent Draws Boundaries Take the actual project miniagent as an example.\nflowchart TB UI[\"FrontendManagement / Workplace\"] API[\"API Layerapp/api\"] subgraph DOMAIN[\"Business Domains / Bounded Contexts\"] USER[\"User \u0026 Permission DomainUser / Role / Permission\"] AGENT[\"Agent DomainAgent / Agent-Tool / Agent-User\"] KB[\"Knowledge Base DomainKnowledge Base / Document / Retrieval\"] TOOL[\"Tool DomainTool / Web Search / SQL Agent\"] CONV[\"Conversation DomainConversation / Session / Message\"] MODEL[\"Model DomainLLM / Embedding / Router Config\"] end SERVICE[\"Business Service Layerapp/services\"] RUNTIME[\"Runtime Capabilitiesapp/runtimeAgentRunner / Retrieval / LLM\"] REPO[\"Data Access Layerapp/repositories\"] INFRA[\"Infrastructure Layerapp/infra\"] DATA[\"Data \u0026 External ResourcesSQLite / DuckDB / ChromaDB / BM25 / Files / LLM APIs\"] UI --\u003e|\"REST / SSE\"| API API --\u003e SERVICE SERVICE --\u003e USER SERVICE --\u003e AGENT SERVICE --\u003e KB SERVICE --\u003e TOOL SERVICE --\u003e CONV SERVICE --\u003e MODEL AGENT --\u003e|\"Binding / Collaboration\"| TOOL AGENT --\u003e|\"Authorization Relationship\"| USER AGENT --\u003e|\"Retrieval Capability\"| KB AGENT --\u003e|\"Using Model\"| MODEL CONV --\u003e|\"Running Agent\"| AGENT SERVICE --\u003e RUNTIME SERVICE --\u003e REPO RUNTIME --\u003e REPO REPO --\u003e INFRA RUNTIME --\u003e INFRA INFRA --\u003e DATA miniagent may not be a textbook DDD implementation, but it already has one characteristic that is extremely important for AI programming:\nThe core business capabilities of the system have been clearly identified.\nminiagent\u0026rsquo;s core capabilities include:\nAgent Model Knowledge Base Tool SQL Agent Permission Conversation System Configuration The backend is further divided into:\napp/api/ app/services/ app/runtime/ app/repositories/ app/schemas/ app/infra/ Where:\napi handles HTTP routing; services handle business logic; runtime handles Agent, Session, LLM, Retrieval, and other runtime components; repositories handle async data access; schemas handle data models; infra handles databases, caching, and infrastructure. This effectively establishes two types of boundaries for AI: business boundaries and technical layer boundaries.\n1. Agent Is an Explicit Business Context Take Agent management as an example.\nminiagent\u0026rsquo;s API file is:\napp/api/admin/agent.py The Service is:\napp/services/admin/agent.py The API file itself states its purpose very clearly:\n# Agent API Router – HTTP layer only, # all logic lives in AgentService That is:\nRouter is only responsible for HTTP. Business logic belongs to AgentService.\nFor example, creating an Agent:\n@router.post(\u0026#34;\u0026#34;) async def create_agent( payload: AgentCreate, svc: AgentService = Depends(get_service), caller_id: int = Depends(_add), ): agent_out = await svc.create_agent(payload) return ApiResponse(data=agent_out) The Router does not:\nDirectly INSERT into the database Manage caching on its own Maintain Tools on its own Instead, it delegates Agent business to:\nAgentService 2. AgentService Is Responsible for Agent\u0026rsquo;s Own Business In AgentService, the code explicitly states:\nclass AgentService: \u0026#34;\u0026#34;\u0026#34; Encapsulates all business logic for the Agent resource. \u0026#34;\u0026#34;\u0026#34; That is:\nAgent\u0026rsquo;s business logic is concentrated in AgentService.\nFor example, updating an Agent:\nasync def update_agent( self, agent_id: int, payload: AgentUpdate ) -\u0026gt; AgentOut: agent = await self._agent_db.update_agent( agent_id, payload.model_dump(exclude_unset=True) ) if agent is None: raise AgentNotFoundError(agent_id) updated = await self._agent_db.get_agent(agent_id) self._cache.on_agent_changed(agent_id) return AgentOut.model_validate(updated) Here we can see:\nAgent modification ↓ Agent data access ↓ Agent cache invalidation ↓ Return Agent model All of these are orchestrated by AgentService.\n3. Cross-Domain Collaboration Also Has Explicit Entry Points An Agent can\u0026rsquo;t only deal with itself forever.\nFor example, an Agent may:\nBind User Bind Tool Bind LLM This is exactly where domain boundaries are most interesting: miniagent doesn\u0026rsquo;t stuff all data logic into the API because of this.\nFor example, when binding Tools:\nasync def update_agent_tools( self, agent_id: int, tool_ids: list[int] ) -\u0026gt; None: unique_tool_ids = list(dict.fromkeys(tool_ids)) tools = await self._tool_db.get_tools_by_ids( unique_tool_ids ) found_tool_ids = {tool.id for tool in tools} missing_tool_ids = [ tool_id for tool_id in unique_tool_ids if tool_id not in found_tool_ids ] if missing_tool_ids: raise ToolNotFoundError(missing_tool_ids) await self._agent_tool_relation_db.update_agent_tools( agent_id, unique_tool_ids, ) self._cache.on_agent_changed(agent_id) This already reflects a very important idea:\nAgent is responsible for orchestrating the business action of \u0026ldquo;binding Tools to Agent.\u0026rdquo;\nDetermining whether Tools exist, updating the Agent-Tool relationship, and refreshing the cache — these are concentrated in the Agent business entry point, rather than scattered across pages, routers, and database models.\n4. API Only Expresses Business Actions The corresponding HTTP API is very simple:\n@router.put(\u0026#34;/{agent_id}/tools\u0026#34;) async def update_agent_tools( agent_id: int, data: AgentToolUpdate, svc: AgentService = Depends(get_service), caller_id: int = Depends(_edit), ): await svc.update_agent_tools( agent_id, data.tool_ids ) return ApiResponse() From a reader\u0026rsquo;s perspective, this code is very easy to understand:\nReceive request ↓ Check permissions ↓ Delegate to AgentService ↓ Return result This is the value that boundaries bring.\nVII. What Does This Project Structure Mean for AI? Now suppose we tell AI:\nAdd a new configuration field to Agent.\nWhen AI enters miniagent, it can follow a very clear path to search:\nAgent Schema ↓ Agent API ↓ Agent Service ↓ Agent Database / Repository If the requirement is:\nModify the knowledge base retrieval logic.\nIt should focus on:\nKnowledge Base Retrieval Pipeline Vector Store BM25 Rather than going off to modify the Agent management page or User permission code.\nThus the entire project gradually forms:\nRequirement ↓ Identify the belonging domain ↓ Enter the corresponding context ↓ Modify according to existing layers ↓ Cross-domain collaboration through explicit interfaces when necessary This is the very real value of DDD for AI programming.\nVIII. Don\u0026rsquo;t Mistake DDD for \u0026ldquo;Creating a Few More Folders\u0026rdquo; There\u0026rsquo;s a very common misconception here.\nSome people think:\nuser/ agent/ tool/ knowledge_base/ Creating a few directories means you\u0026rsquo;re doing DDD.\nOf course not. What truly matters is:\nWhether there are stable business responsibilities behind those directories.\nIf:\nAgentService does everything:\nUsers Permissions Knowledge Base Tools Logging Email System Configuration Then no matter how pretty the directory names are, it can still be a \u0026ldquo;Big Ball of Mud.\u0026rdquo;\nSo what truly matters is:\nA business concept ↓ Clear responsibilities ↓ Clear boundaries ↓ Clear collaboration methods Conclusion DDD is a large software design discipline. But for AI programming, we don\u0026rsquo;t need to master everything at once:\nEntity Value Object Aggregate Domain Event Domain Service Repository The most important first step is really just one thing:\nFirst tell AI what business domains this system is composed of.\nThen further tell it:\nWhich domain you are currently modifying, and which areas not to touch.\nIt\u0026rsquo;s like giving AI a city map. Without a map:\nAI goes wherever it can find a path.\nWith domain boundaries:\nUser is a zone Agent is a zone Knowledge Base is a zone Tool is a zone Conversation is a zone Cross-zone movement isn\u0026rsquo;t forbidden, but must follow the \u0026ldquo;official roads.\u0026rdquo;\nThus:\nDDD\u0026rsquo;s greatest value for AI is not adding design complexity, but reducing the unbridled freedom in the world of code.\nWhen AI can generate thousands or even tens of thousands of lines of code per day, these boundaries become increasingly important.\nFirst define the domains, then let AI solve problems within those domains.\nThis is exactly the key method for preventing AI from gradually turning large projects into \u0026ldquo;Big Balls of Mud.\u0026rdquo;\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0104/","summary":"\u003cp\u003eGetting AI to write a single feature is usually not difficult. What\u0026rsquo;s truly difficult is:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eAfter AI writes dozens or even hundreds of features in a row, can the code still remain clear?\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eMany projects start with a decent structure — a bit of user functionality here, some permission handling there, order features over there, logging features somewhere else.\u003c/p\u003e\n\u003cp\u003eAs requirements grow, different modules start calling each other, referencing each other, modifying each other, and may eventually end up like this:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003euser.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003epermission.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eagent.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003etool.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eknowledge_base.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ↓\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003econversation.py\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  ↘\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  calls user.py again\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThere are plenty of files and lots of code, but who is responsible for what becomes increasingly unclear.\u003c/p\u003e\n\u003cp\u003eSoftware architecture has a very vivid name for this:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBig Ball of Mud\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAnd Domain-Driven Design, or \u003cstrong\u003eDDD\u003c/strong\u003e as we often hear, has one very important role: drawing boundaries clearly before the system starts to get messy.\u003c/p\u003e\n\u003cp\u003eFor AI programming, this role is especially critical.\u003c/p\u003e\n\u003chr\u003e","title":"Domain-Driven Design: Drawing Context Boundaries for AI to Say Goodbye to \"Big Ball of Mud\" Code"},{"content":"AI is very good at writing code. But without clear architectural boundaries, it can easily develop one problem:\nWherever it\u0026rsquo;s convenient to write code, that\u0026rsquo;s where the code gets written.\nBusiness rules that should belong to the backend might end up stuffed into Vue pages. API calls that should be uniformly encapsulated might scatter across various components. The frontend might even \u0026ldquo;invent\u0026rdquo; its own data structures inconsistent with the backend, based on page requirements.\nThe code might run, but the project gradually loses its boundaries.\nAnd frontend-backend separation is the first important boundary to draw for AI.\n📌 Technical Profile Frontend-Backend Separation\nThe frontend is primarily responsible for page rendering, user interaction, and client-side state, while the backend handles business logic, permissions, security, and data storage.\nThe two sides do not directly intrude into each other\u0026rsquo;s internals, but communicate through stable APIs (i.e., Application Programming Interfaces).\n💡 One-Sentence Understanding You can think of a software system as a restaurant.\nThe frontend is the dining hall: responsible for menu display, reception, taking orders, and presenting results to customers; The backend is the kitchen: responsible for ingredients, cooking, inventory, and actual business processing; The API is the order ticket. A waiter can deliver an order to the kitchen, but shouldn\u0026rsquo;t run in and start cooking. A chef is responsible for cooking, but shouldn\u0026rsquo;t run out to the dining hall and modify the menu page.\nTherefore:\nThe essence of frontend-backend separation is not about putting code into two folders, but about clearly defining responsibility boundaries.\nThis is especially important for AI programming.\nBecause human programmers usually know:\nEven though this code works when written here, it shouldn\u0026rsquo;t be written here.\nAI, without such architectural rules, tends to prioritize:\nThe approach that completes the current task the fastest.\nOver time, this easily leads to duplicate interfaces, business logic sinking into the frontend, scattered permission checks, inconsistent data structures, and other problems.\nI. Architecture Standards: First Tell AI Who Is Responsible for What To make AI truly follow frontend-backend separation, you can\u0026rsquo;t just tell it one sentence:\nThis project adopts a frontend-backend separation architecture. This statement is too abstract. A more effective approach is to turn it into rules that AI can directly execute.\nBelow is a diagram illustrating the front-end and back-end separation architecture of miniagent:\nFurthermore, these can be solidified into the following rules.\nRule 1: The Frontend Must Not Directly Access the Database The database belongs to the backend.\nWhen the frontend needs data, it can only obtain it through APIs.\nThe frontend doesn\u0026rsquo;t need to know whether the database (e.g., SQLite, MySQL, PostgreSQL) has been replaced.\nRule 2: Core Business Rules Are Authoritative on the Backend For example:\nIs the user allowed to delete?\nThis check must not only exist on the frontend:\nif (user.role !== \u0026#34;admin\u0026#34;) { disableDeleteButton() } The frontend can hide the button to improve user experience.\nBut the real permission check must happen on the backend, otherwise users can completely bypass the page and call the API directly.\nTherefore:\nThe frontend is responsible for experience, the backend is responsible for authority.\nRule 3: The Frontend Should Not \u0026ldquo;Invent\u0026rdquo; Its Own APIs Suppose AI is developing:\nDelete a user.\nWithout rules, it might directly write in a component:\naxios.delete(`/user/delete?id=${id}`) Another AI on the backend might implement it as:\nDELETE /api/v1/admin/users/{id} Both pieces of code look fine individually.\nBut together, they don\u0026rsquo;t work.\nSo the rule should be:\nBefore adding new frontend features, first check existing APIs;\nBefore adding new backend APIs, first check the existing interface specifications.\nRule 4: API Calls Must Be Uniformly Encapsulated Don\u0026rsquo;t let pages be littered with:\naxios.get(...) axios.post(...) fetch(...) Instead, form:\nVue Component ↓ API Module ↓ HTTP Client ↓ Backend API This way, logic such as Token management, error handling, timeouts, Token refresh, and logging can all be handled uniformly.\nUsing a mature open-source frontend framework is a shortcut: it has already uniformly encapsulated common methods for interacting with the backend.\nRule 5: The Backend Must Not In Turn Depend on Specific Pages What the backend provides is:\nUsers Knowledge Base Agent Sessions Tools Permissions Not:\n\u0026#34;Data for the user management page\u0026#34; \u0026#34;Data for the card on the right side of the homepage\u0026#34; \u0026#34;Data for a certain button\u0026#34; Pages change easily, but capabilities should be kept stable as much as possible.\nTherefore, the backend should provide business APIs, rather than designing business logic around a specific Vue page.\nII. What Are the Benefits of Doing This? Frontend-backend separation sounds like a technical \u0026ldquo;division of labor,\u0026rdquo; but its benefits are actually very intuitive.\nEspecially in AI programming, its greatest value is not \u0026ldquo;looking more standardized,\u0026rdquo; but making the entire project less likely to get messier as it grows.\n1. AI Can More Easily Know Where Code Should Be Written Without clear boundaries, when AI receives a requirement, it easily writes wherever is convenient.\nFor example, adding a \u0026ldquo;whether the user can delete\u0026rdquo; check.\nWithout rules, AI might write the check directly into the frontend page. With frontend-backend separation, responsibilities become clear:\nFrontend: Whether to show the delete button Backend: Whether this user actually has delete permission AI doesn\u0026rsquo;t need to guess each time, and the project becomes more stable.\n2. Modifying the Frontend Won\u0026rsquo;t Easily Break the Backend Too A system\u0026rsquo;s interface changes frequently. Today the button is on the left, tomorrow it moves to the right. Today it\u0026rsquo;s a table, tomorrow it becomes cards.\nIf business logic is all mixed into pages, every interface change might accidentally damage core functionality.\nAfter frontend-backend separation:\nHow the page displays -\u0026gt; Decided by the frontend How the business actually executes -\u0026gt; Decided by the backend As long as the API doesn\u0026rsquo;t change, both sides can be modified relatively independently.\n3. Multiple Frontends Can Reuse the Same Backend This is also a very practical aspect of frontend-backend separation.\nThe same backend can simultaneously serve:\nAdmin dashboard Regular user website Mobile App Mini-program Other systems Because they all call the same set of APIs.\nIn other words, the backend provides \u0026ldquo;capabilities,\u0026rdquo; not a specific page.\n4. AI Is Less Likely to Reinvent the Wheel Without unified interface standards, AI easily writes a new set for every feature:\nRequest methods Error handling Permission checks Response formats API addresses Over time, a project might end up with many different approaches.\nWith frontend-backend separation combined with a unified API Client, AI can more easily reuse existing patterns.\nFor example:\nPage -\u0026gt; Existing API Module -\u0026gt; Unified HTTP Client -\u0026gt; Backend This way, new features usually only need to extend the existing structure, rather than reinventing a new solution.\n5. Easier to Troubleshoot When Problems Arise If an error occurs after a user clicks a button, you can follow a clear chain to investigate:\nDid the page send a request? -\u0026gt; Did the API receive it? -\u0026gt; Did the business logic execute? -\u0026gt; Did the database return correctly? It\u0026rsquo;s easier to determine which layer the problem is in.\nThe same applies to AI. You can directly tell it:\n\u0026ldquo;The frontend request is normal, the backend returns 500, please only check the backend.\u0026rdquo;\nAI\u0026rsquo;s search scope will be significantly narrowed.\n6. Better Suited for Collaborative Development with Multiple People and AI The development approach that will become increasingly common in the future is likely not one programmer writing from start to finish, but:\nHuman developer Technical lead Frontend developer Backend developer AI programming assistant AI Agent All participating in one project together.\nAt this point, the biggest fear is not that everyone can\u0026rsquo;t write code, but:\nEveryone writes code according to their own understanding.\nClear frontend-backend boundaries are like pre-marked construction zones. Who is responsible for what, where the interfaces are, how data is exchanged — all are relatively clear.\nTherefore:\nFrontend-backend separation is not just dividing code, it\u0026rsquo;s dividing responsibility.\nAnd the clearer the responsibility, the easier it is for AI to participate stably in large-scale project development.\nIII. Prompt Implementation: Truly Telling AI About Architectural Boundaries Therefore, we can write the frontend-backend separation rules into project-level rules, for example:\n## Frontend/Backend Boundaries This project strictly follows the frontend-backend separation principle. Frontend responsibilities: - UI rendering - User interaction - Client-side state management - Form validation (to improve user experience) - Calling backend APIs Backend responsibilities: - Business logic - Authorization - Security validation - Data persistence - Database access Rules: - Frontend code must never directly access the database. - Core business rules must not exist only in frontend code. - Frontend and backend can only communicate through defined APIs. - Reuse the project\u0026#39;s existing HTTP client and API modules. - Follow existing API paths and response patterns. - Before creating a new API, check whether an existing API can be reused. - Backend code must not depend on specific frontend pages or components. Then when asking AI to develop features in the future, it\u0026rsquo;s no longer just:\nImplement the delete user feature. But:\nImplement the delete user feature under the existing frontend-backend separation architecture. Strictly follow the project\u0026#39;s existing Frontend / Backend Boundaries, API specifications, and HTTP Client encapsulation. First check existing interfaces and directory structure, prioritize reusing existing implementations, do not create new calling conventions on your own. The two prompts may look like they only differ by a few sentences.\nBut the context given to AI is completely different.\nThe first tells AI:\nWhat I want.\nThe second simultaneously tells AI:\nWhat I want, and within what boundaries you must complete it.\nSo:\nPrompts are not a replacement for architecture, but the vehicle for conveying architecture to AI.\nIV. Positive Output: See How miniagent Does It Take the actual project miniagent as an example.\nminiagent is an agent platform, and the project itself adopts a clear frontend-backend separation structure:\nminiagent/ ├── backend/ # FastAPI backend ├── management/ # PureAdmin management console └── workplace/ # Regular user workspace Where:\nbackend uses FastAPI; management uses Vue 3 + TypeScript + PureAdmin; workplace is an independent Vue 3 user client. Both frontends obtain business capabilities through FastAPI APIs, rather than directly accessing the database. miniagent\u0026rsquo;s README also clearly specifies the structure of Admin → API, Workplace → API, then into the application core and data layer.\n1. The Backend Uniformly Defines API Boundaries For example, miniagent\u0026rsquo;s user management API is uniformly mounted in FastAPI at:\napp.include_router( admin_user_router, prefix=\u0026#34;/api/v1/admin/users\u0026#34;, tags=[\u0026#34;Admin - User\u0026#34;] ) That is:\n/api/v1/admin/users is the user management boundary defined by the backend.\nThe specific delete user API is:\n@router.delete( \u0026#34;/{user_id}\u0026#34;, response_model=ApiResponse, summary=\u0026#34;Delete user\u0026#34; ) async def delete_user( user_id: int, svc: UserService = Depends(get_service), caller_id: int = Depends(_delete), ): await svc.delete(user_id) return ApiResponse() There are several very important architectural signals here.\nFirst, permissions belong to the backend:\ncaller_id: int = Depends(_delete) Deleting a user shouldn\u0026rsquo;t assume the user has delete permission just because the frontend displayed a \u0026ldquo;delete button.\u0026rdquo; The real permission is still verified by the backend.\nSecond, the API layer doesn\u0026rsquo;t directly access the database either:\nawait svc.delete(user_id) It delegates the actual business operation to:\nUserService Thus forming:\nFrontend ↓ FastAPI Route ↓ UserService ↓ Repository ↓ Database This clearly delineates responsibilities layer by layer.\n2. The Frontend Uniformly Accesses the Backend Through API Modules miniagent\u0026rsquo;s management console also doesn\u0026rsquo;t let pages arbitrarily construct backend addresses.\nFor example, the login API:\nexport const getLogin = (data?: object) =\u0026gt; { return http.request\u0026lt;UserResult\u0026gt;( \u0026#34;post\u0026#34;, baseUrlApi(\u0026#34;login\u0026#34;), { data } ); }; Token refresh also reuses the unified HTTP Client:\nexport const refreshTokenApi = (data?: object) =\u0026gt; { return http.request\u0026lt;RefreshTokenResult\u0026gt;( \u0026#34;post\u0026#34;, baseUrlApi(\u0026#34;refresh-token\u0026#34;), { data } ); }; That is, business code uniformly goes through:\nhttp.request(...) Rather than each Vue page creating its own axios or fetch call.\nThe API prefix isn\u0026rsquo;t scattered and hardcoded either, but uniformly defined:\nexport const baseUrlApi = (url: string) =\u0026gt; `/api/v1/${url}`; So:\nbaseUrlApi(\u0026#34;login\u0026#34;) Produces:\n/api/v1/login This effectively gives AI a very clear signal:\nWhen you need to call the backend, extend along the existing API layer, rather than starting from scratch in a component.\n3. The Backend Internally Maintains Its Own Layering miniagent doesn\u0026rsquo;t simply split Vue and Python into two directories and call it done.\nIts backend is internally further divided into:\napp/api/ HTTP interfaces app/services/ Business logic app/runtime/ Agent runtime app/repositories/ Data access app/schemas/ Data models app/infra/ Infrastructure These responsibilities are also clearly explained in the project README. So the entire system actually forms:\nflowchart TD M[\"Management\"] W[\"Workplace\"] API[\"FastAPI API\"] S[\"Services\"] R[\"Runtime / Repository\"] D[\"Database / Vector DBDuckDB / Files ...\"] M --\u003e|\"REST / SSE\"| API W --\u003e|\"REST / SSE\"| API API --\u003e S S --\u003e R R --\u003e D When AI next receives:\nAdd user features Add knowledge base features Add Agent management features\nFor such requirements, it no longer faces a blank slate, but a project with pre-marked construction zones:\nWhere to write pages Where to write APIs Where to write business logic Where to write database operations Where to check permissions How the frontend accesses APIs All have existing boundaries to follow.\nConclusion The greatest significance of frontend-backend separation for AI programming is not splitting the project into:\nfrontend/ backend/ Two directories.\nWhat\u0026rsquo;s truly important is that it clearly tells AI for the first time:\nWhat belongs to the frontend, what belongs to the backend, and how the two can only collaborate.\nThe stronger AI\u0026rsquo;s capabilities and the more code it can modify at once, the more important these boundaries become.\nBecause excellent software architecture is never about limiting development efficiency. On the contrary:\nArchitecture trades the freedom to make mistakes for the freedom to do things right.\nThe same is true for AI programming.\nFirst draw the boundaries, then let AI exercise its capabilities within those boundaries.\nThis is what makes frontend-backend separation truly worth reunderstanding in the AI era.\nOpen Source Code github gitee 🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0103/","summary":"\u003cp\u003eAI is very good at writing code. But without clear architectural boundaries, it can easily develop one problem:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eWherever it\u0026rsquo;s convenient to write code, that\u0026rsquo;s where the code gets written.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eBusiness rules that should belong to the backend might end up stuffed into Vue pages. API calls that should be uniformly encapsulated might scatter across various components. The frontend might even \u0026ldquo;invent\u0026rdquo; its own data structures inconsistent with the backend, based on page requirements.\u003c/p\u003e\n\u003cp\u003eThe code might run, but the project gradually loses its boundaries.\u003c/p\u003e\n\u003cp\u003eAnd \u003cstrong\u003efrontend-backend separation\u003c/strong\u003e is the first important boundary to draw for AI.\u003c/p\u003e\n\u003chr\u003e","title":"Frontend-Backend Separation: Constraining AI's Division of Labor to Avoid Interface Coupling and Responsibility Confusion"},{"content":"In the previous article, we discussed a core principle:\nDefine boundaries and rules first, then let AI exercise its capabilities within those boundaries.\nSo here comes the question:\nHow can these architecture rules actually be handed over to AI?\nThe answer is the topic of this article — Project Rules.\n📌 Technology Snapshot Project Rules\nRefers to providing AI with a project\u0026rsquo;s tech stack, architecture boundaries, coding standards, and security requirements on an ongoing basis through project-level rule files.\nDifferent AI coding tools may call them Rules, Instructions, Custom Instructions, etc., but the core purpose is the same:\nTell AI how this project should be written, and which boundaries must not be crossed.\n💡 One-Sentence Summary Project Rules are like the \u0026ldquo;Construction Specifications\u0026rdquo; posted at the entrance of a construction site.\nAI is the construction crew, the architecture is the blueprint, and the Rules explicitly tell it:\nWhich walls can be torn down, which floors cannot be crossed, what standards materials must meet, and what inspections must pass before completion.\nThese rules participate in the AI coding process as persistent project context or instructions, continuously influencing code generation, modification, and Agent behavior.\n1. How Mainstream AI Coding Tools Define Project Rules Different tools vary in file names and rule mechanisms, but the essence is the same: Turn the standards that originally relied on engineers\u0026rsquo; memory into project constraints that AI can read continuously.\nHere are the most common configuration approaches today:\nTool Common Project Rule File Primary Purpose Cursor .cursor/rules/*.mdc Project-level / path-level rules GitHub Copilot .github/copilot-instructions.md Repository-level rules GitHub Copilot .github/instructions/*.instructions.md Path-level rules Windsurf .windsurf/rules/*.md Workspace / path-level rules General Agent AGENTS.md Project or directory-level rules 2. How to Write a High-Constraint Python Rule File What AI needs are specific, explicit, and verifiable rules — rules where you can clearly tell whether they\u0026rsquo;ve been violated.\n❌ Vague rule: - Please keep the code elegant and loosely coupled. ✅ Explicit rule: - `routers/` only handles HTTP requests and responses, and must not directly access the database. A standard Python engineering rule file should include the following 4 core modules:\nModule Question Answered Project Context What kind of project is this? Architecture Boundaries Where should each type of code go? Coding Standards How should the code be written? Validation \u0026amp; Safety What things must not be done? Example: Python Project Rule File When generating or modifying code, follow the project rules below. ## 1. Project Context Tech stack: - Python 3.11+ - FastAPI - Pydantic v2 - SQLAlchemy 2.x - Pytest Priorities: - Clear responsibilities - Type safety - Testability - Loose coupling Do not introduce new third-party dependencies unless explicitly needed. ## 2. Architecture Boundaries The project follows: Router → Service → Repository → Database Responsibilities: - `routers/`: Handle HTTP requests, parameter validation, and responses - `services/`: Handle business logic - `repositories/`: Responsible for database access - `schemas/`: Define Pydantic input/output models - `models/`: Define database ORM models Prohibited: - Routers directly accessing the database - Services directly executing SQL - Repositories containing business logic - Repositories calling Services in reverse - Services depending on Routers ## 3. Coding Standards - Public functions must provide complete type hints - API input/output must use Pydantic for validation - External dependencies such as databases and HTTP clients must be provided through parameters or dependency injection - Prefer `async / await` for I/O operations - Hardcoding API keys, passwords, and other sensitive configurations is prohibited - Prioritize reusing existing components; avoid duplicate code - Do not over-engineer for \u0026#34;what might be needed later\u0026#34; ## 4. Validation \u0026amp; Safety - All external input must be validated - Silently swallowing exceptions with `except Exception: pass` is prohibited - Use ORM or parameterized queries for databases - Writing passwords, tokens, or API keys into source code or logs is prohibited - Use high-risk operations such as `shell=True`, `eval()`, and `exec()` with caution ## When AI Modifies Code Before modifying code: 1. First determine which layer the code belongs to 2. Check whether a reusable implementation already exists in the project 3. Only modify the code necessary to complete the current task If a user\u0026#39;s request conflicts with the existing architecture: **First point out the conflict and risks, then provide an implementation plan that conforms to the existing architecture.** Many advanced Agents already have project rules built in and reinforced. When using them in practice, trim as needed.\n3. Project Rules Are Guardrails, Not Compilers One thing to pay special attention to:\nProject rules can improve the consistency of AI output, but they cannot guarantee that AI will follow them 100%.\nTruly reliable engineering constraints should be:\nAI generates code ↓ Lint (code style check) ↓ Type Check (type checking) ↓ Unit Tests (unit testing) ↓ Security Check (security scanning) ↓ CI (automatically runs the above checks) ↓ Pass → Allow merge Fail → Reject merge Project rules are responsible for telling AI what to do; testing and inspection tools are responsible for checking whether it actually did it.\nDifferent AI coding tools already have varying degrees of built-in code standards, context management, and security mechanisms, so real projects don\u0026rsquo;t need to mechanically copy templates. Only keep the rules that you truly need AI to follow long-term.\nProject rules are not about \u0026ldquo;the more the better\u0026rdquo; — the clearer, more stable, and more project-aligned they are, the better.\nConclusion The value of Project Rules is not to make AI \u0026ldquo;write prettier code,\u0026rdquo; but to make it consistently work according to the same set of engineering rules.\nIt takes the conventions that originally lived in the architect\u0026rsquo;s head: Project Context, Architecture Boundaries, Coding Standards, Validation \u0026amp; Safety\nAnd transforms them into project context that AI can read continuously.\nArchitecture is responsible for defining boundaries, project rules are responsible for telling AI what those boundaries are, and testing and inspection tools ensure the boundaries are not breached.\n🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0102/","summary":"\u003cp\u003eIn the previous article, we discussed a core principle:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eDefine boundaries and rules first, then let AI exercise its capabilities within those boundaries.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eSo here comes the question:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eHow can these architecture rules actually be handed over to AI?\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eThe answer is the topic of this article — \u003cstrong\u003eProject Rules\u003c/strong\u003e.\u003c/p\u003e\n\u003chr\u003e","title":"How to Define Coding Boundaries for AI Using Project Rules"},{"content":"With the development of large language models such as GPT, Claude, Gemini, and Qwen, software development is entering a new era. Coding tasks that once took hours or even days can now be completed by AI in a matter of minutes. However, many teams have noticed a puzzling phenomenon:\nThe more code gets written, the faster the project falls apart.\nThe reason is not that AI isn\u0026rsquo;t smart enough, but rather:\nA lack of architectural constraints.\nOne important reason modern AI Agents can operate relatively stably is that: they do not let large language models run wild without any constraints.\nMature Agent systems typically define clear behavioral boundaries for the model through mechanisms such as System Instructions (i.e., system prompts), Tool Specifications (i.e., tool specs), permission control, workflows, state management, and result validation.\nIn essence, this is highly consistent with the philosophy of software architecture:\nDefine the boundaries and rules first, then let AI exercise its capabilities within those boundaries. Even a highly capable model, when lacking clear constraints, dealing with ever-expanding context, or wielding excessive tool permissions, may gradually develop issues such as format drift, responsibility overreach, incorrect tool invocations, or inconsistent code style.\n📌 Technical Profile Software Architecture The top-level structure and organizational principles of a software system. It defines:\nHow the system is decomposed How modules collaborate How responsibilities are divided How the system evolves The goal is to ensure that the software maintains the following qualities as it scales:\n✅ Maintainable ✅ Scalable ✅ Testable ✅ Evolvable\n💡 In a nutshell Architecture design is like a construction blueprint.\nAI can be the fastest construction worker in the world, but without a blueprint, it will most likely just keep stacking bricks higher and higher\u0026hellip;\n1. Plain-Language Breakdown: Why Relying More on AI Means You Can\u0026rsquo;t \u0026ldquo;Let Go of the Reins\u0026rdquo; If you\u0026rsquo;re not familiar with programming, think of writing code as \u0026ldquo;building a house\u0026rdquo;:\nThe old development model: You had to lay every brick yourself (write every line of code by hand). It was slow, but because you placed each brick personally, you knew exactly where the load-bearing walls were.\nThe AI model today: AI has become a construction worker with superhuman strength. You say \u0026ldquo;build me a kitchen,\u0026rdquo; and in half a minute it hauls over a stack of pre-built walls.\nSounds wonderful, right? But that\u0026rsquo;s exactly where the problem lies.\nAI\u0026rsquo;s Inherent Shortcomings 1. AI Is \u0026ldquo;Near-Sighted\u0026rdquo; — It Naturally Lacks a Long-Term, Big-Picture Perspective Modern AI programming tools can already read large numbers of project files and even search entire code repositories.\nThe real problem is:\nAI\u0026rsquo;s judgment heavily depends on the Context it is currently given (i.e., the context, which has a length limit for large models).\nIf a project lacks clear module boundaries, architecture documentation, and engineering standards, it\u0026rsquo;s very difficult for AI to reliably infer the design intent of the entire system just from scattered code.\nSo when you ask it to \u0026ldquo;add one more feature,\u0026rdquo; it easily tends to prioritize the local solution that completes the current task most easily, rather than the solution best suited for the long-term evolution of the system.\n2. Bad Structures Get Quickly Replicated and Amplified by AI AI is very good at finding patterns from existing code.\nThis is normally an advantage, but it also means:\nGood architecture gets replicated, and bad architecture gets replicated just the same.\nIf a project already has muddled responsibilities, duplicated code, and unreasonable dependencies, AI will likely follow these patterns and continue to spread them at a speed far exceeding manual coding.\nA few dozen lines of \u0026ldquo;code smell\u0026rdquo; can quickly evolve into thousands of lines of unmaintainable \u0026ldquo;big ball of mud\u0026rdquo; code.\n3. AI Won\u0026rsquo;t Automatically Fill In All Security Boundaries AI\u0026rsquo;s primary task is usually to complete the current instruction, and many security requirements in a project don\u0026rsquo;t automatically appear in the prompt.\nFor example, if you only ask:\nWrite a login API endpoint. Without further specifying the authentication method, password storage, input validation, permission model, and exception handling, the generated code — even if it \u0026ldquo;runs\u0026rdquo; — may not meet the security requirements of a production environment.\nTherefore, security standards also need to explicitly become part of architectural constraints.\n2. Architecture Standards: The New Role of Human Engineers Now that the \u0026ldquo;heavy lifting\u0026rdquo; of coding has been taken over by AI, the role of human engineers, technical managers, and even cross-disciplinary developers has fundamentally shifted: you\u0026rsquo;ve been promoted from \u0026ldquo;bricklayer\u0026rdquo; to \u0026ldquo;chief construction commander.\u0026rdquo;\nCore Idea: From \u0026ldquo;Doing It Yourself\u0026rdquo; to \u0026ldquo;Setting the Rules for AI\u0026rdquo; In the era of AI programming, architecture design is the \u0026ldquo;code of conduct\u0026rdquo; you issue to AI:\nDraw boundaries (no wandering into others\u0026rsquo; territory): Clearly tell AI that the code responsible for the UI must not directly touch the database, and the code responsible for billing must not mix in SMS-sending logic. Set standards (no cutting corners): Forbid AI from hardcoding database passwords in the source, and mandate that all errors must be returned in a unified format. Build the skeleton (fill-in-the-blank development): First, you or a standard architecture template erects the \u0026ldquo;reinforced concrete skeleton\u0026rdquo; of the building, so that AI only needs to \u0026ldquo;tile the floors\u0026rdquo; and \u0026ldquo;place furniture\u0026rdquo; in the designated rooms. 🎯 In a sentence\nGive AI room to发挥 its abilities, but set clear boundaries first.\n3. Turning Architecture Standards into Project Rules In the past, architecture standards typically lived in design documents, Wikis, or architects\u0026rsquo; heads. In the era of AI programming, an important change is: These standards can directly become project-level instructions that AI reads every time it writes code.\nModern AI IDEs (i.e., integrated development tools, such as Cursor, Windsurf, Claude Code, etc.) already support project-level rules.\nFor example:\nCursor: .cursor/rules/*.mdc GitHub Copilot: .github/copilot-instructions.md General Agent spec: AGENTS.md Claude Code: CLAUDE.md These files are essentially:\nTranslating architecture documents into System Prompts (i.e., system prompts) that AI can understand.\nThis way, every time AI is about to generate code, it first checks this set of rules, making the model more consistently adhere to project conventions.\nExample: Enterprise-Grade Python Architecture Rules You are a senior Python architect who strictly follows enterprise-grade software engineering standards. When generating any code for this project, you must enforce the following **Three Architecture Principles**: 1. **No Responsibility Mixing (Layered Isolation)** - The view/interface layer (Router) responsible for receiving user requests **must not** contain specific business computation logic or directly operate on the database. - The core business logic layer (Service) must remain pure and **must not** be directly aware of the HTTP protocol or request details. 2. **Decoupling and Modularization** - Replaceable infrastructure dependencies such as databases, HTTP clients, LLM clients, and repositories should not be hardcoded and instantiated within core business logic. 3. **Common Functionality Extraction and Type Safety** - Use Pydantic for API boundaries, tool parameters, configuration objects, and data structures requiring runtime validation; prefer native Type Hints for simple internal module data. - Common functionalities such as authentication, logging, and error handling must call existing shared components in the project — **never** write redundant code in business logic. If a user\u0026#39;s instruction would violate the above architecture principles, proactively remind the user and suggest improvements that comply with the architecture standards, rather than directly generating non-compliant code. 4. Practical Comparison Requirement: Build an Agent that receives a user instruction (e.g., \u0026ldquo;List the files in the current directory\u0026rdquo;), automatically triggers a bash tool to execute the command, feeds the command-line output back to the model, and ultimately returns a generated answer.\n1. Without Architecture Constraints If you simply tell the AI:\nWrite a Python script that calls OpenAI to execute a bash command and passes the result back to the model. AI will very likely stuff SDK initialization, JSON Schema authoring, tool execution, and the two-step conversation loop all into a single function without a second thought:\n❌ AI running free: all logic crammed into one function.\nimport os import json import subprocess from openai import OpenAI def process_user_request(user_prompt: str) -\u0026gt; str: # 1. Tight coupling: the business flow directly depends on the OpenAI SDK and a specific model client = OpenAI(api_key=os.environ.get(\u0026#34;OPENAI_API_KEY\u0026#34;)) messages = [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: user_prompt}] # 2. Messy data structures: tool definitions hardcoded inside the function tools = [{ \u0026#34;type\u0026#34;: \u0026#34;function\u0026#34;, \u0026#34;function\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;run_bash\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Run bash commands\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: {\u0026#34;command\u0026#34;: {\u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;}}, \u0026#34;required\u0026#34;: [\u0026#34;command\u0026#34;] } } }] # First LLM request response = client.chat.completions.create( model=\u0026#34;gpt-4o\u0026#34;, messages=messages, tools=tools ) response_message = response.choices[0].message # 3. Hardwired control flow: hand-written, verbose tool_calls triggering and result-passing logic if response_message.tool_calls: messages.append(response_message) for tool_call in response_message.tool_calls: if tool_call.function.name == \u0026#34;run_bash\u0026#34;: args = json.loads(tool_call.function.arguments) # Bare subprocess run, no directory sandboxing or exception protection result = subprocess.check_output(args[\u0026#34;command\u0026#34;], shell=True).decode() messages.append({ \u0026#34;role\u0026#34;: \u0026#34;tool\u0026#34;, \u0026#34;tool_call_id\u0026#34;: tool_call.id, \u0026#34;content\u0026#34;: result }) # Second LLM request: feed the result back to regenerate an answer final_response = client.chat.completions.create( model=\u0026#34;gpt-4o\u0026#34;, messages=messages ) return final_response.choices[0].message.content return response_message.content 2. With Architecture Constraints If following the architecture design standards of miniagent:\nTools are independently decoupled via the `@tool` decorator (high cohesion); the Agent and LLMClient are composed through dependency injection (low coupling); the Tool Call loop is automatically orchestrated inside the Agent. ✅ miniagent: each module has a single responsibility, the code organization is clean and extremely concise.\n# 1. Independent tool module (tools.py): type-safe, isolated workspace from pydantic import BaseModel, Field from miniagent.tools import tool import subprocess class BashInput(BaseModel): command: str = Field(description=\u0026#34;The bash command to execute\u0026#34;) @tool(name=\u0026#34;bash\u0026#34;, description=\u0026#34;Execute a bash command with the specified working directory\u0026#34;) def create_bash_tool(workspace: str = \u0026#34;./\u0026#34;): def execute(input_data: BashInput) -\u0026gt; str: # Specify a default working directory; note: cwd is not equivalent to a security sandbox return subprocess.check_output( input_data.command, shell=True, cwd=workspace ).decode() return execute # 2. Core runtime module (main.py): model and Agent decoupled, pipeline in two lines import asyncio from miniagent import Agent, LLMClient from tools import create_bash_tool async def main(): # 1. Dependency injection: LLM is independently abstracted — switching to DeepSeek or Anthropic only needs a config change llm = LLMClient(provider=\u0026#34;openai\u0026#34;, model=\u0026#34;gpt-4o\u0026#34;) # 2. Composition and assembly: the Agent automatically manages the model, system prompt, and tool set\u0026#39;s callback loop agent = Agent( llm_client=llm, tools=[create_bash_tool(workspace=\u0026#34;./sandbox\u0026#34;)] ) # 3. Minimal interaction: automatically completes [call LLM -\u0026gt; execute tool -\u0026gt; pass back result -\u0026gt; output final answer] result = await agent.run(\u0026#34;List the files in the current directory\u0026#34;) print(result.final_answer) if __name__ == \u0026#34;__main__\u0026#34;: asyncio.run(main()) Side-by-Side Comparison of the Same Functionality Dimension ❌ No clear architecture constraints ✅ With clear architecture constraints Responsibility boundaries Easily drifts with each new requirement Clear module responsibilities Swapping the LLM Business code bound to the SDK LLMClient isolates the specific provider Adding new tools Schema and Tool Loop keep piling up Tools registered independently Tool loop Maintained by business code itself Handled uniformly by the Agent Runtime Testability Components hard to isolate Tool / Client / Agent can be tested separately AI output consistency Different sessions easily adopt different structures Rules + architecture template constrain the output Conclusion: AI Boosts Speed, Architecture Determines Direction Large language models have changed how code is produced, but they haven\u0026rsquo;t changed the fundamental laws of software engineering.\nWhat determines whether a system can evolve over the long term is still:\nArchitecture design Module boundaries Engineering standards System abstraction AI accelerates development; architecture determines how far the software can go.\n⭐ The New Division of Labor Between Humans and AI AI is better at micro-level implementation:\nWriting functions Writing modules Writing CRUD Filling in tests Refactoring local code Humans should focus on macro-level decisions:\nUnderstanding the business Designing the architecture Dividing modules Setting constraints Reviewing key decisions The architecture plan itself can certainly involve AI in the design and discussion.\nBut the final decision on what the system should look like — and the accountability for that decision — should still rest with humans.\nArchitecture is not a shackle on AI; it is the boundary and navigation system that enables AI to perform at a high quality.\n🪐 Good luck 🪐\n","permalink":"http://www.wfcoding.com/en/articles/design/0101/","summary":"\u003cp\u003eWith the development of large language models such as GPT, Claude, Gemini, and Qwen, software development is entering a new era.\nCoding tasks that once took hours or even days can now be completed by AI in a matter of minutes.\nHowever, many teams have noticed a puzzling phenomenon:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eThe more code gets written, the faster the project falls apart.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThe reason is not that AI isn\u0026rsquo;t smart enough, but rather:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eA lack of architectural constraints.\u003c/strong\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eOne important reason modern AI Agents can operate relatively stably is that: \u003cstrong\u003ethey do not let large language models run wild without any constraints.\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eMature Agent systems typically define clear behavioral boundaries for the model through mechanisms such as System Instructions (i.e., system prompts), Tool Specifications (i.e., tool specs), permission control, workflows, state management, and result validation.\u003c/p\u003e\n\u003cp\u003eIn essence, this is highly consistent with the philosophy of software architecture:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eDefine the boundaries and rules first, then let AI exercise its capabilities within those boundaries.\u003c/strong\u003e\nEven a highly capable model, when lacking clear constraints, dealing with ever-expanding context, or wielding excessive tool permissions, may gradually develop issues such as \u003cstrong\u003eformat drift, responsibility overreach, incorrect tool invocations, or inconsistent code style\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e","title":"The More You Rely on AI to Write Code, the Less You Can Afford to Skip Architecture Design"}]